/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <journal/app/keys.h>
#include <journal/views/notebook.h>
#include <journal/app/constants.h>
#include <journal/common/utils.hpp>
#include <journal/views/scene_references.h>
#include <journal/views/input_references.h>
#include <baja/linear_interpolation.hpp>
#include <journal/views/quad_renderer.h>
#include <journal/views/directx/util.hpp>
#include <journal/common/util.h>
#include <journal/views/winrt/util.hpp>
#include <journal/views/shared.hpp>
#include <dependencies/includes/austin_private.h>

namespace journal { namespace view {

notebook::notebook(
    const size_t pageNumber,
    const std::shared_ptr<model::inotebook_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    ) :
    _notebookModel(model),
    _inputCapture(inputCapture),
    _notebookObserverCookie(-1),
    _layoutId(layout_id::none),
    _pageNumber(pageNumber),
    _settingsObserverCookie(-1),
    _pageCache(constants::general::pageViewCacheCapacity()),
    _pageSet(NO_PAGE, NO_PAGE),
    _pageSetDirty(false),
    _cameraObserverCookie(-1),
    _cameraChangeNumber(static_cast<b::uint64>(-1)),
    _state(state::idle),
    _subState(sub_state::none),
    _keepAppBarAfterResorting(false),
    _transitionProgress(1.f),
    _expansionProgress(1.f),
    _refreshPageSet(false),
    _openingNotebook(false),
    _saveThumbnails(true)
    {
        _pageCache.setPurgeHandler(std::bind(&notebook::onPagePurged, this, std::placeholders::_1, std::placeholders::_2));
    }

std::shared_ptr<notebook> 
notebook::create(
    const size_t pageNumber,
    const std::shared_ptr<model::inotebook_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    )
{
    std::shared_ptr<notebook> returnValue = std::make_shared<notebook>(pageNumber, model, inputCapture);
    returnValue->init();

    constants::automationService()->registerRootView(std::dynamic_pointer_cast<b::ibase>(std::dynamic_pointer_cast<a::iinput_observer>(returnValue->shared_from_this())));

    return std::dynamic_pointer_cast<notebook>(returnValue->shared_from_this());
}
    
void notebook::init()
{
    common::validatePageCacheSize();

    _stackState = _finalStackState = stack_state::stacked;

    // observe the model
    _notebookObserverCookie = _notebookModel->addObserver(
        std::dynamic_pointer_cast<model::inotebook_model_observer>(shared_from_this()));

    // set up the scene state
    // set up lights
    _sceneState.setAmbient(constants::general::ambientLightColor());

    _sceneState.getLight()->camera()->setOffsetDistance(constants::general::lightDistance());
    _sceneState.getLight()->camera()->setAngles(constants::general::lightAngles());
    _sceneState.getLight()->camera()->setFocus(constants::general::lightFocus());
    _sceneState.getLight()->camera()->setOffsetXY(constants::general::lightOffsetXY());

    _sceneState.getCamera()->setOffsetDistance(constants::general::cameraDistance());
    
    // create the scene graph
    _sceneGraph = gs::createSceneGraph();

    // create font for zoom level rendering
    std::shared_ptr<g::iasset_manager> assetManager = constants::graphics()->getAssetManager();
    _font = assetManager->getOrCreateFont(
        constants::graphics(), 
        constants::references::fontName(),
        constants::references::fontSize(),
        false // right to left
        );

    this->initializeBackgroundTextures();

    _backRenderer = quad_renderer::create();

    // create scene references
    _sceneReferences = createSceneReferences(
        std::static_pointer_cast<a::iinput_capture>(shared_from_this()), 
        &_sceneState
        );

    // create input references
    _inputReferences = input_references::create();

    // observe settings service
    _settingsObserverCookie = constants::settings()->addObserver(
        std::dynamic_pointer_cast<isettings_service_observer>(shared_from_this()));

    this->initializeLayouts();

    // observe camera
    _cameraObserverCookie = _sceneState.getCamera()->addObserver(std::dynamic_pointer_cast<icamera_observer>(shared_from_this()));

    // curl animation
    _pageCurl = page_curl::create();

    // create the scroll bars
    _scrollBars = scroll_bars::create();

    // initialize the cover and calculate the start scale
    this->initializeCover(&_startingScale);

    _notebookCamera = createNotebookCamera(std::dynamic_pointer_cast<inotebook_camera_driver>(shared_from_this()));

    constants::uiService()->addObserver(std::dynamic_pointer_cast<a::iui_service_observer>(shared_from_this()));
    this->setLayoutId(layout_id::full); // always start in full mode

    this->updateCameraService();

    // store pointer to back buffer
    g::render_target snapRenderTarget;
    snapRenderTarget.snap(constants::graphics());
    _finalScreenTexture = std::dynamic_pointer_cast<g::itexture>(snapRenderTarget.renderTarget());
}

void notebook::initializeBackgroundTextures()
{
    // create the background staging texture
    g::viewport vp;
    constants::graphics()->getViewport(&vp);

    vp.width = constants::general::backgroundTextureDimensions().x;
    vp.height = constants::general::backgroundTextureDimensions().x;

    _backTexture1 = constants::graphics()->createTexture(
        static_cast<b::uint32>(vp.width),
        static_cast<b::uint32>(vp.height),
        1,
        static_cast<b::uint32>(g::texture_usage::renderTarget),
        g::pixel_format::d32BitColorNoAlpha,
        g::memory_pool::default,
        1, // single sampled
        0
        );

    // create the background final texture
    _backTexture2 = constants::graphics()->createTexture(
        static_cast<b::uint32>(vp.width),
        static_cast<b::uint32>(vp.height),
        1,
        static_cast<b::uint32>(g::texture_usage::renderTarget),
        g::pixel_format::d32BitColorNoAlpha,
        g::memory_pool::default,
        1, // single sampled
        0
        );

    _backRenderTarget = std::make_shared<g::render_target>(vp, nullptr, nullptr);
}

void notebook::initializeCover(b::float32* startingScale)
{
    _coverTexture = constants::graphics()->getAssetManager()->getOrCreateTextureFromFile(
        constants::graphics(),
        constants::general::coverFileName(), 
        g::pixel_format::fromSource
        );

    *startingScale = constants::animations::startingScale();

    // create cover page node
    _coverNode = paper_sheet_node::create(
        constants::graphics(),
        static_cast<b::uint32>(constants::general::pageWorldDimensions().x * 2) + 1,
        static_cast<b::uint32>(constants::general::pageWorldDimensions().y * 2) + 1,
        1/2.f
        );
    
    _coverNode->setPosition(m::vector3d(0, 0, 0.01f));
    _coverNode->setDefaultEffect(g::standard_effect::meshPhong);

    std::shared_ptr<g::imaterial> material = _coverNode->getMaterial();
    material->setSpecularPower(FLT_MAX); // matte
    material->setAmbientColor(g::packColor(255, 0, 0, 0));
    material->setTexture(_coverTexture);

    _sceneGraph->addChild(_coverNode);

    // register camera service
    b::services::set<notebook>(std::dynamic_pointer_cast<icamera_service>(shared_from_this()));
}

void notebook::initializeLayouts()
{
    // populate zoom levels
    _layoutData.clear();

    // remember the width of 1:1 ratio to be used as a reference
    b::float32 windowWorldWidth = constants::shellService()->windowSize().x / constants::shellService()->logicalDpi();

    // calculate some ratios that we'll need
    b::float32 aspectRatioWorld = constants::general::pageWorldDimensions().x / constants::general::pageWorldDimensions().y;
    b::float32 aspectRatioPixel = constants::shellService()->windowSize().x / constants::shellService()->windowSize().y;
    b::float32 aspectRatioPercentage = aspectRatioPixel / aspectRatioWorld;

    // full
    std::shared_ptr<layout_data> data = std::make_shared<layout_data>();
    b::float32 scale;
    data->id = layout_id::full;
    data->pagePixelResolution = constants::general::pagePixelDimensions(); // TRACK: 77
    data->name = L"full page";
    data->loadLevel = view::load_level::full;

    if (aspectRatioPercentage >= 1.0f)
    {
        scale = windowWorldWidth / (constants::general::pageWorldDimensions().x * aspectRatioPercentage);
    }
    else
    {
        scale = windowWorldWidth / (constants::general::pageWorldDimensions().x);
    }

    b::float32 fullScale = scale;

    data->layout = createListPageLayout(
        scale,
        (_stackState == stack_state::stacked) ? true : false, // stacked
        shared_from_this()
        );

    _layoutData.insert(std::make_pair(data->id, data));
 
    // fit to width
    data = std::make_shared<layout_data>();
    data->id = layout_id::fitToWidth;
    data->pagePixelResolution = constants::general::pagePixelDimensions(); // TRACK: 77
    data->name = L"fit to width";
    data->loadLevel = view::load_level::full;
    if (aspectRatioPercentage >= 1.0f)
    {
        scale = windowWorldWidth / constants::general::pageWorldDimensions().x;
    }
    else
    {
        scale = windowWorldWidth / (constants::general::pageWorldDimensions().x * aspectRatioPercentage);
    }

    // if fit-to-width scale is the same as full (should never really be less), make sure
    // it's always a bit larger
    if (scale <= fullScale)
    {
        scale = fullScale * 1.4f; // (40% larger)
    }

    data->layout = createListPageLayout(
        scale,
        (_stackState == stack_state::stacked) ? true : false, // stacked
        shared_from_this()
        );

    _layoutData.insert(std::make_pair(data->id, data));

    // grid
    data = std::make_shared<layout_data>();
    data->id = layout_id::grid;
    data->pagePixelResolution = constants::general::pagePixelDimensions(); // TRACK: 77
    data->name = L"grid";
    data->loadLevel = view::load_level::thumbnail;
    data->layout = createGridPageLayout(constants::general::gridLayoutPageCountY(), shared_from_this());

    _layoutData.insert(std::make_pair(data->id, data));
}

void notebook::onPointer(const a::pointer_args& args, void* context)
{
    bool* handled = static_cast<bool*>(context);
    std::shared_ptr<a::iinput_observer> target = _capture.get();

    _idleTime.reset();

    // if one of our children is capturing the input, pass it down directly to it
    if (target)
    {
        target->onPointer(args, static_cast<void*>(handled));
    }
    else
    {
        if (!*handled)
        {
            if (_state == state::idle &&
                _layoutId == layout_id::grid &&
                args.type == a::pointer_event_type::down && 
                args.buttons == (b::uint32)a::pointer_button::right &&
                args.deviceType == a::device_type::mouse
                )
            {
                *handled = this->startSelection(args.point);
            }
            
            if (!*handled &&
                _state == state::selectingPages &&
                args.type == a::pointer_event_type::down &&
                args.buttons == (b::uint32)a::pointer_button::left &&
                args.deviceType == a::device_type::mouse
                )
            {
                *handled = this->handleSelectionTap(args.point);
            }

            if (!*handled &&
                _state == state::selectingWithLasso ||
                _state == state::selectingWithLassoIdle
                )
            {
                *handled = this->handleLasso(args);
            }

            if (
                !*handled &&
                _state == state::idle
                )
            {
                for (auto &iter : _slots)
                {
                    iter.second.pageView->onPointer(args, static_cast<void*>(handled));
                    if (*handled)
                    {
                        break;
                    }
                }
            }
        }
    }

    if (!*handled) _sceneReferences->onPointer(args, handled);

    _inputReferences->onPointer(args);
}

void notebook::onResize(const a::resize_args& args)
{
    common::validatePageCacheSize();

    size_t currentPageNumber = _pageNumber;

    this->updateCameraService();

    this->initializeBackgroundTextures();

    this->initializeLayouts();

    _sceneState.getCamera()->refresh(constants::graphics());

    // this will make sure the pages get repositioned as needed
    _pageSetDirty = true;
    this->updatePageSet(_sceneState.getCamera()->offsetXY(), _sceneState.getCamera()->focus());

    _notebookCamera->moveToPage(
        this->currentLayout(), 
        currentPageNumber,  
        false, 
        constants::animations::pageMoveDuration(),
        constants::animations::pageMoveAcceleration()
        );
}

void notebook::onScreenChange(const a::screen_change_args& args)
{
    // noop: pages shouldn't care about this
}

void notebook::onVisibilityChanged(const bool visible)
{
}

void notebook::onGesture(const a::gesture_args& args, void* context)
{
    BAJA_VERIFY(context);
    bool* handled = static_cast<bool*>(context);
    std::shared_ptr<a::iinput_observer> target = _capture.get();

    _idleTime.reset();

    // if one of our children is capturing the input, pass it down directly to it
    if (target)
    {
        target->onGesture(args, static_cast<void*>(handled));
    }
    else
    {
        *handled = this->handleGesture(args);

        if (!*handled)
        {
            std::find_if(
                _slots.begin(), _slots.end(),
                [&](const std::pair<size_t, page_slot>& pair) -> bool
            {
                pair.second.pageView->onGesture(args, static_cast<void*>(handled));
                return *handled;
            });
        }
    }

    if (!*handled) _sceneReferences->onGesture(args, static_cast<void*>(handled));
}

bool notebook::startSelection(const m::point2d& position)
{
    bool returnValue = false;

    b::float32 t = constants::general::currentTime();
    for (auto &iter : _slots)
    {
        size_t pageNumber = iter.first;
        page_slot& slot = iter.second;

        m::point3d localPoint;
        if (slot.pageView->pixelHitTest(position, &localPoint))
        {
            BAJA_VERIFY(_selections.size() == 0);

            b::float32 pageSelectDuration = constants::animations::pageSelectDuration();
            b::float32 pageCountY = (b::float32) constants::general::gridLayoutPageCountY();
            size_t firstPageShownNumber = _slots.begin()->first;

            // Disable every page except for the page we are looking at
            for (auto &iter2 : _slots)
            {
                size_t pageNumber2 = iter2.first;
                page_slot &slot = iter2.second;
                page_selection &selection = _selections[pageNumber2];
                b::float32 duration = pageSelectDuration;

                if (pageNumber == pageNumber2)
                {
                    selection.startTransition(duration, true);
                    selection.makeSelected();
                    slot.pageView->setSelected(true);
                }
                else
                {
                    duration += (pageNumber2 - firstPageShownNumber) / pageCountY * 80.0f;
                    selection.startTransition(duration, false);
                }

                _state = state::startingSelecting;
                returnValue = true;
            }
        }
    }

    return returnValue;
}

bool notebook::handleGesture(const a::gesture_args& args)
{
    if (args.deviceType != a::device_type::touch)
    {
        return false;
    }

    if (_state == state::idle)
    {
        if (args.type == a::gesture_event_type::hold && _layoutId == layout_id::grid)
        {
            this->startSelection(args.position);
            constants::uiService()->showAppBar(a::app_bar_id::grid);
        }
        else if (args.type == a::gesture_event_type::doubleTap)
        {
            for (auto &iter : _slots)
            {
                page_slot& slot = iter.second;

                m::point3d localPoint;
                if (slot.pageView->pixelHitTest(args.position, &localPoint))
                {
                    size_t pageNumber = slot.pageView->model()->pageNumber();
                    bool goingCurl = false;

                    if (this->allowPageCurling())
                    {
                        b::float32 p = (localPoint.x + constants::general::pageWorldDimensions().x / 2.f) / constants::general::pageWorldDimensions().x;
                        if (p < constants::general::pageTurnTapRegion() && _pageNumber > 0)
                        {
                            this->autoCurlThisPageAway(false);
                            goingCurl = true;
                        }
                        if (p > (1.f - constants::general::pageTurnTapRegion()) && _pageNumber < _notebookModel->pageCount() - 1)
                        {
                            this->autoCurlThisPageAway(true);
                            goingCurl = true;
                        }
                    }
                    if (goingCurl)
                    {
                        ;
                    }
                    else if (_layoutId == layout_id::fitToWidth)
                    {
                        // in fit-to-width layout, a double tap always brings us back if in the middle of the screen
                        // otherwise around the edges it causes a page flip
                        b::float32 p = (localPoint.x + constants::general::pageWorldDimensions().x / 2.f) / constants::general::pageWorldDimensions().x;

                        if (p < constants::general::pageTurnTapRegion() && _pageNumber > 0)
                        {
                            _notebookCamera->moveToPage(
                                this->currentLayout(), 
                                _pageNumber - 1,  
                                true, // keep y
                                constants::animations::pageMoveDuration(),
                                constants::animations::pageMoveAcceleration()
                                );
                        }
                        else if (p > (1.f - constants::general::pageTurnTapRegion()) && _pageNumber < _notebookModel->pageCount() - 1)
                        {
                            _notebookCamera->moveToPage(
                                this->currentLayout(), 
                                _pageNumber + 1,  
                                true, // keep y
                                constants::animations::pageMoveDuration(),
                                constants::animations::pageMoveAcceleration()
                                );
                        }
                        else // tapped in the middle region
                        {
                            this->setLayoutIdWithStack(this->getNextLayout(zoom_direction::out));
                        }
                    }
                    else if (_pageNumber == pageNumber)
                    {
                        this->setLayoutIdWithStack(this->getNextLayout(zoom_direction::in));
                    }
                    else // tapped on a different page
                    {
                        _pageNumber = pageNumber;

                        // for grid views, zoom in
                        if (_layoutId == layout_id::grid)
                        {
                            this->setLayoutIdWithStack(this->getNextLayout(zoom_direction::in));
                        }
                        else
                        {
                            // for all other views, move the camera
                            _notebookCamera->moveToPage(
                                this->currentLayout(), 
                                _pageNumber,  
                                false,
                                constants::animations::pageMoveDuration(),
                                constants::animations::pageMoveAcceleration()
                                );
                        }
                    }
                    return true;
                }
            }
        }
        else if (args.type == a::gesture_event_type::start)
        {
            if (this->allowPageCurling() && (args.type == a::gesture_event_type::start))
            {
                _slots[_pageNumber].pageView->pixelHitTest(args.position, &_startCurlBackwardsPoint);
                b::float32 p = (_startCurlBackwardsPoint.x + constants::general::pageWorldDimensions().x / 2.f) / constants::general::pageWorldDimensions().x;

                if (_pageNumber > 0 && p < constants::general::pageCurlRegion())
                {
                    // Curl backwards. Can't call attachPage because the slot hasn't been enabled yet -- hence
                    // the curlingBackwards state.
                    _state = state::curlingBackwards;

                    return true;                       
                }
                else if (p > (1.f - constants::general::pageCurlRegion()))
                {
                    // Curl forwards
                    _pageCurl->attachPage(
                        _slots[_pageNumber].pageView, 
                        _pageNumber + 1 >= _notebookModel->pageCount(), 
                        page_curl::paper_type::looseLeaf
                        );
                    
                    this->updatePageSet(
                        this->currentLayout()->calculateCameraOffsetXY(_pageNumber), 
                        this->currentLayout()->focus()
                        );

                    _state = state::curling;
                    
                    return true;
                }
            } 
            
            _inputCapture.lock()->setCapture(std::dynamic_pointer_cast<a::iinput_observer>(shared_from_this()));
            _startOffsetXY = _sceneState.getCamera()->offsetXY();
            _startScale = _sceneState.getCamera()->scale();
            _state = state::panning;

            if (_layoutId == layout_id::fitToWidth)
            {
                _startPage = _pageNumber;
            }

            return true;
        }
    }
    else if (_state == state::panning)
    {
        if (args.type == a::gesture_event_type::update)
        {
            b::float32 ty = abs(args.cumulative.translation.y); 
            b::float32 tx = abs(args.cumulative.translation.x); 

            if (ty / tx > 5 && 
                tx < 50 &&
                ty > 100 && 
                constants::shellService()->touchPointCount() == 1 &&
                _layoutId == layout_id::full)
            {
                _finalStackState = (_stackState == stack_state::expanded) ? stack_state::stacked : stack_state::expanded;
                this->setStackExpansion(_finalStackState);
            }
            else
            {
                // tilt the camera on the y axis with scroll velocity
                if (_layoutId == layout_id::grid || _stackState == stack_state::expanded)
                {
                    scroll_result scrollResult = scrollCamera(constants::general::cameraDistance(), args.linearVelocity.x, _sceneState.getCamera()->angles());
                    _notebookCamera->setDistance(scrollResult.distance);
                    _notebookCamera->setAngles(scrollResult.angles);
                }

                if (abs(1.f - args.cumulative.scale) > 0)
                {
                    // pinch gesture: change layouts
                    b::float32 f = abs(1.f - args.cumulative.scale);
                    zoom_direction direction = (args.cumulative.scale > 1.f) ? zoom_direction::in : zoom_direction::out;

                    // only change page when zooming in.  when zooming out, stay
                    if (direction == zoom_direction::in)
                    {
                        for (auto iter : _slots)
                        {
                            page_slot& slot = iter.second;

                            // set the page number to the page where the user pinched
                            m::point3d localPoint;
                            if (slot.pageView->pixelHitTest(args.position, &localPoint))
                            {
                                _pageNumber = slot.pageView->model()->pageNumber();
                                break;
                            }
                        }
                    }

                    // handle the situation when we are expanded and going to/from grid
                    layout_id nextLayoutId = this->getNextLayout(direction);

                    this->setLayoutIdWithStack(nextLayoutId);
                }
                else
                {
                    // show scrollbars
                    _scrollBars->setVisibility(true);

                    m::point2d offsetXY;

                    if (_layoutId == layout_id::fitToWidth)
                    {
                        // in fit-to-width mode, we want to move a maximum of the entire width of the page
                        b::float32 px = (args.cumulative.translation.x / constants::shellService()->windowSize().x);
                        offsetXY.x = _startOffsetXY.x - (px * constants::general::pageWorldDimensions().x);
                    }
                    else
                    {
                        offsetXY.x = _startOffsetXY.x - (args.cumulative.translation.x / this->currentLayout()->scale()) * 0.02f;
                    }

                    offsetXY.y = _startOffsetXY.y + (args.cumulative.translation.y / this->currentLayout()->scale()) * 0.02f;
                
                    this->currentLayout()->constrainOffsetXY(offsetXY);

                    _notebookCamera->setOffsetXY(offsetXY);
                    return true;
                }
            }
        }
        else if (args.type == a::gesture_event_type::complete)
        {
            _inputCapture.lock()->releaseCapture();
            this->goIdle();
            if (_layoutId == layout_id::fitToWidth && _stackState == stack_state::expanded)
            {
                size_t nextPage = _startPage;
                b::float32 diff = (_sceneState.getCamera()->offsetXY().x - _startOffsetXY.x) / constants::general::pageWorldDimensions().x;
                if (diff < -constants::general::pageTurnDragDistance() && nextPage > 0) // offset then do the page turn
                {
                    nextPage--;
                }
                else if (diff > constants::general::pageTurnDragDistance() && nextPage < _notebookModel->pageCount() - 1)
                {
                    nextPage++;
                }

                if (nextPage != _startPage)
                {
                    _notebookCamera->moveToPage(
                        this->currentLayout(), 
                        nextPage,  
                        true, // keep y
                        constants::animations::pageMoveDuration(),
                        constants::animations::pageMoveAcceleration()
                        );
                }
            }
            else // ! fit-to-width
            {
                _notebookCamera->reset(_layoutData[_layoutId]->layout, constants::animations::quickDuration());
            }

            return true;
        }
    }
    else if (_state == state::transitioningToGrid)
    {
        if (args.type == a::gesture_event_type::complete)
        {
            _inputCapture.lock()->releaseCapture();
        }
    }
    else if (_state == state::startingSelecting)
    {
        return true;
    }
    else if (_state == state::selectingPages)
    {
        b::float32 t = constants::general::currentTime();

        BAJA_VERIFY(_selections.size() > 0);
        if (args.type == a::gesture_event_type::tap)
        {
            return this->handleSelectionTap(args.position);
        }
        else if (args.type == a::gesture_event_type::start)
        {
            if (_selections.size() == 1)
            {
                // If the one page that's selected has finished its 
                // being-selected animation, start moving the page.
                
                auto &iter = _selections.begin();
                size_t pageNumber = iter->first;
                page_selection &selection = iter->second;
                if (selection.isSelected())
                {
                    m::point3d localPoint;
                    if (_slots.find(pageNumber) != _slots.end() && _slots[pageNumber].pageView->pixelHitTest(args.position, &localPoint))
                    {
                        _inputCapture.lock()->setCapture(std::dynamic_pointer_cast<a::iinput_observer>(shared_from_this()));

                        const std::shared_ptr<paper_sheet_node> &pageNode = 
                            std::static_pointer_cast<paper_sheet_node>(_slots[pageNumber].pageView->sceneNode());
                        pageNode->getLocalPosition(&_startPoint);
                        _startOffsetXY = args.position;

                        _state = state::movingSelectedPage;
                        return true;
                    }
                }
            }

            _inputCapture.lock()->setCapture(std::dynamic_pointer_cast<a::iinput_observer>(shared_from_this()));
            _startOffsetXY = _sceneState.getCamera()->offsetXY();
            _startScale = _sceneState.getCamera()->scale();
            _state = state::selectingPagesPanning;
            return true;
        }
    }
    else if (_state == state::selectingPagesPanning)
    {
        if (args.type == a::gesture_event_type::update)
        {
            m::point2d offsetXY;
            offsetXY.x = _startOffsetXY.x - (args.cumulative.translation.x / this->currentLayout()->scale()) * 0.02f;
            offsetXY.y = _startOffsetXY.y + (args.cumulative.translation.y / this->currentLayout()->scale()) * 0.02f;
            this->currentLayout()->constrainOffsetXY(offsetXY);
            _notebookCamera->setOffsetXY(offsetXY);
            return true;
        }
        else if (args.type == a::gesture_event_type::complete)
        {
            _inputCapture.lock()->releaseCapture();
            _state = state::selectingPages;
            return true;
        }
    }
    else if (_state == state::movingSelectedPage)
    {
        if (args.type == a::gesture_event_type::update)
        {
            auto &iter = _selections.begin();
            size_t pageNumber = iter->first;
            
            const std::shared_ptr<paper_sheet_node> &pageNode = 
                std::static_pointer_cast<paper_sheet_node>(_slots[pageNumber].pageView->sceneNode());

            b::float32 tx = args.position.x - _startOffsetXY.x; // in pixel coords
            b::float32 ty = args.position.y - _startOffsetXY.y; // in pixel coords

            // focus == size of scene in world coordinates
            m::vector2d focus = _sceneState.getCamera()->focus();

            tx = tx * focus.x / constants::shellService()->windowSize().x;    // in world coords
            ty = ty * (-focus.y) / constants::shellService()->windowSize().y; // in world coords

            pageNode->setPosition(m::vector3d(_startPoint.x + tx, _startPoint.y + ty, _startPoint.z));
        }
        else if (args.type == a::gesture_event_type::complete)
        {
            auto &iter = _selections.begin();
            int oldPageNumber = (int)iter->first;

            // calculate what its page should be from its position

            m::point3d pagePosition;
            const std::shared_ptr<paper_sheet_node> &pageNode = 
                std::static_pointer_cast<paper_sheet_node>(_slots[oldPageNumber].pageView->sceneNode());
            pageNode->getLocalPosition(&pagePosition);
            int newPageNumber = (int)this->currentLayout()->calculatePageNumber(pagePosition);
            newPageNumber = max(newPageNumber, 0);
            newPageNumber = min(newPageNumber, (int)(_notebookModel->pageCount() - 1));
            
            if (oldPageNumber != newPageNumber)
            {
                // Update the model
                _notebookModel->movePage(oldPageNumber, newPageNumber);
                
                // No need to recompute _selections, since only one page is selected
                _selections[newPageNumber].makeSelected();
                _selections[oldPageNumber].makeDeselected();

                // Update _slots, needed so the transform matrices are correct for pixelHitTest
                auto fromNumberSlot = _slots[oldPageNumber]; // gotta be a copy
                if (oldPageNumber < newPageNumber)
                {
                    for (int i=oldPageNumber; i<newPageNumber; i++)
                    {
                        _slots[i] = _slots[i + 1];
                    }
                }
                else
                {
                    for (int i=oldPageNumber; i>newPageNumber; i--)
                    {
                        _slots[i] = _slots[i - 1];
                    }					
                }
                _slots[newPageNumber] = fromNumberSlot;
            }

            _inputCapture.lock()->releaseCapture();

            // set up transition animation
            b::float32 t = constants::general::currentTime();
            _transitionProgress = 0.f;
            _transitionAnimationEndTime = t + constants::animations::pageMoveDuration();
            _transitionAnimation = b::tv_interpolation(
                t,
                _transitionAnimationEndTime,
                _transitionProgress,
                1 - _transitionProgress,
                constants::animations::pageMoveAcceleration().x,
                constants::animations::pageMoveAcceleration().y
                );

            _state = state::finishedMovingSelectedPage;
        }
    }
    else if (_state == state::curling)
    {
        m::point3d localPoint;
        _slots[_pageNumber].pageView->pixelHitTest(args.position, &localPoint);

        if (args.type == a::gesture_event_type::update)
        {
            _pageCurl->startUserCurl(localPoint.x, localPoint.y);
        }
        else // a::gesture_event_type::complete
        {
            _pageCurl->startAutoCurl(true);
            _state = state::autoCurling;
        }
        return true;
    }
    else if (_state == state::curlingBackwards)
    {
        return true;
    }
    else if (_state == state::finishedMovingSelectedPage)
    {
        // flop pages into place using an animation
        return true;
    }

    return false;
}

bool notebook::handleSelectionTap(const m::point2d& position)
{
    // TRACK 499: Could be a perf problem if you have hundreds of selected pages. Consider
    // keeping track of numPagesSelected as a static var.
    size_t numPagesSelected = 0;
    for (auto &iter : _selections)
    {
        page_selection& selection = iter.second;
        if (selection.isSelected() || selection.isSelecting())
        {
            numPagesSelected++;
        }
    }

    // See what page we're tapping and if it's to be enabled or disabled

    size_t pageNumberTapped = -1;
    bool enablingPage = false;
    bool disablingPage = false;
    bool tappedPage = false;
    for (auto &iter : _slots)
    {
        size_t pageNumber = iter.first;
        page_slot& slot = iter.second;

        m::point3d localPoint;
        if (slot.pageView->pixelHitTest(position, &localPoint))
        {
            pageNumberTapped = pageNumber;
            page_selection &selection = _selections[pageNumber];
            if (selection.isSelected())
            {
                disablingPage = true;
            }
            else
            {
                enablingPage = true;
            }
            tappedPage = true;
            break;
        }
    }

    if ((disablingPage && numPagesSelected == 1) || !tappedPage)
    {
        this->deselectAll();
        return true;
    }
    else if (disablingPage)
    {
        _selections[pageNumberTapped].startTransition(constants::animations::pageSelectDuration(), false);
        return true;
    }
    else if (enablingPage)
    {
        _selections[pageNumberTapped].startTransition(constants::animations::pageSelectDuration(), true);
        return true;
    }

    return false;
}

void notebook::onCameraChange(const std::shared_ptr<icamera>& c)
{
    this->updateCameraService();

    // don't do anything if we are doing a transition to avoid blips (pages appearing and disappearing)
    if (_state == state::transitioningFromGrid || _state == state::transitioningToGrid || _state == state::transitioningOther) 
    {
        return;
    }

    // if the camera changed, re-calculate the page set
    if (_cameraChangeNumber != _sceneState.getCamera()->changeNumber())
    {
        this->updatePageSet(_sceneState.getCamera()->offsetXY(), _sceneState.getCamera()->focus());
        this->updatePageNumberInternal();
        _cameraChangeNumber = _sceneState.getCamera()->changeNumber();
    }
}

void notebook::onKey(const a::key_args& args, void* context)
{
    BAJA_VERIFY(context);
    bool* handled = static_cast<bool*>(context);
    std::shared_ptr<a::iinput_observer> target = _capture.get();

    // if one of our children is capturing the input, pass it down directly to it
    if (target)
    {
        target->onKey(args, static_cast<void*>(handled));
    }
    else
    {
        if (_state == state::idle)
        {
            if (journal::app::key_registry::check(journal::app::key::addNewPage, args.vk))
            {
                this->addNewPage();
                *handled = true;
            }
            else if (journal::app::key_registry::check(journal::app::key::togglePerf, args.vk))
            {
                DEBUG_CONSOLE_TRACE("Toggling performance monitors");
                std::shared_ptr<ibool_setting> setting = std::dynamic_pointer_cast<ibool_setting>(constants::settings()->findSetting(constants::performance::name::showMonitors));
                bool b = !setting->getValue();
                DEBUG_CONSOLE_TRACE(boost::wformat(L"toggling perf monitors %s") % (b ? L"on" : L"off"));
                constants::settings()->setBool((int)constants::performance::id::showMonitors, b);
            }
            else if (journal::app::key_registry::check(journal::app::key::toggleMemoryMonitors, args.vk))
            {
                DEBUG_CONSOLE_TRACE("Toggling memory tracking monitors");
                std::shared_ptr<ibool_setting> setting = std::dynamic_pointer_cast<ibool_setting>(constants::settings()->findSetting(constants::memory_tracking_settings::name::showMonitors));
                bool b = !setting->getValue();
                DEBUG_CONSOLE_TRACE(boost::wformat(L"toggling memory tracking monitors %s") % (b ? L"on" : L"off"));
                constants::settings()->setBool((int)constants::memory_tracking_settings::id::showMonitors, b);
            }
            else if (journal::app::key_registry::check(journal::app::key::resetPerformanceMonitors, args.vk))
            {
                constants::performanceService()->resetAll(true);
            }
            else if (journal::app::key_registry::check(journal::app::key::resetSettings, args.vk))
            {
                constants::settings()->reset();
            }
            else if (journal::app::key_registry::check(journal::app::key::dumpNotebookModel, args.vk))
            {
                DEBUG_MODEL_DUMP(_notebookModel, true, L"dump\t");
                *handled = true;
            }
            else if (journal::app::key_registry::check(journal::app::key::saveThumbnail, args.vk))
            {
                DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
                    L"SaveThumbnailsAsync: calling explicitly from key binding"
                    );
                this->saveThumbnailsAsync();
                *handled = true;
            }
            else if (journal::app::key_registry::check(journal::app::key::toggleStackExpansion, args.vk))
            {
                _finalStackState = (_stackState == stack_state::expanded) ? stack_state::stacked : stack_state::expanded;
                this->setStackExpansion(_finalStackState);
            }
            else if (journal::app::key_registry::check(journal::app::key::zoomLevelDown, args.vk))
            {
                if (_state == state::idle)
                {
                    this->setLayoutIdWithStack(this->getNextLayout(zoom_direction::in));
                    *handled = true;
                }
            }
            else if (journal::app::key_registry::check(journal::app::key::zoomLevelUp, args.vk))
            {
                if (_state == state::idle)
                {
                    this->setLayoutIdWithStack(this->getNextLayout(zoom_direction::out));
            
                    *handled = true;
                }
            }
            else if (journal::app::key_registry::check(journal::app::key::curlPage, args.vk))
            {
                float theta = constants::page_curling::curlTheta();
                float Ay = constants::page_curling::curlAy();
                float angle = constants::page_curling::curlAngle();
                float conicContribution = constants::page_curling::curlConicContribution();
                _pageCurl->attachPage(_slots[_pageNumber].pageView, false, page_curl::paper_type::looseLeaf);
                _pageCurl->debugCurlPage(page_curl::cone_parameters(theta, Ay, angle, conicContribution));
                *handled = true;
            }
            else if (journal::app::key_registry::check(journal::app::key::resetPageCurl, args.vk))
            {
                _pageCurl->attachPage(_slots[_pageNumber].pageView, false, page_curl::paper_type::looseLeaf);
                _pageCurl->debugCurlPage(page_curl::cone_parameters(90.0f, -70.0f, 0.0f, 1.0f));
                *handled = true;
            }
            else if (journal::app::key_registry::check(journal::app::key::restoreCamera, args.vk))
            {
                _notebookCamera->moveToPage(
                    this->currentLayout(), 
                    _pageNumber,  
                    false,
                    constants::animations::pageMoveDuration(),
                    constants::animations::pageMoveAcceleration()
                    );
                *handled = true;
            }
            else if (journal::app::key_registry::check(journal::app::key::credits, args.vk))
            {
                if (_layoutId != layout_id::grid)
                {
                    constants::automationService()->showCreditsAsync();
                }
                //_debugConsole->setIsVisible(true);
                //this->printCredits();
            }
        }

        if (!*handled) _sceneReferences->onKey(args, handled);
        if (!*handled) _slots[_pageNumber].pageView->onKey(args, handled);
        _inputReferences->onKey(args);
    }
}

void notebook::renderFrame(const bool withChrome)
{
    // apply the scene state
    _sceneState.apply(constants::graphics());

    // update the graph's transforms
    static const m::matrix44 identity;
    _sceneGraph->updateTransforms(identity, identity, false);

    // set up some state
    constants::graphics()->setZCompare(g::compare_state::less);
    constants::graphics()->setCulling(g::cull_state::none);
    constants::graphics()->setZWrite(g::enabled_state::enabled);
    constants::graphics()->setZBuffer(g::enabled_state::enabled);

    if (constants::general::renderShadows())
    {
        // snap current target
        g::render_target snapRenderTarget;
        snapRenderTarget.snap(constants::graphics());

        _backRenderTarget->setRenderTarget(_backTexture1);
        _backRenderTarget->apply(constants::graphics());

        // clear the background
        constants::graphics()->clear(
            g::clear_targets::colorDepthAndStencil,
            constants::general::backgroundPackedColor(),
            1.f, 0);

        for (auto &iter : _slots)
        {
            size_t pageNumber = iter.first;
            page_slot& slot = iter.second;

            // hide shadows when curling in stack mode
            bool hideShadow = false;
            if (_state == state::deletingPages && _selections.find(pageNumber) != _selections.end())
            {
                // and also when deleting a page
                hideShadow = true;
            }

            if (!slot.isRemoved && !hideShadow)
            {
                b::float32 shadowX = 0.4f;

                if (_selections.size() > 0) 
                {
                    b::float32 deselectX = 0.2f;

                    auto &iterSel = _selections.find(pageNumber);
                    if (iterSel == _selections.end())
                    {
                        shadowX = deselectX;
                    }
                    else
                    {
                        page_selection &selection = iterSel->second;

                        if (!selection.isSelected())
                        {
                            // Change the shadow position for pages that are being selected/deselected
                            // Gives a neat 3D look that's really subtle but awesome.
                            b::float32 progress = selection.transitionProgress();
                            b::linear_interpolation li(0.0, 1.0, deselectX, 0.4f);
                            shadowX = li.apply(progress);
                        }
                    }
                }

                b::float32 scaleX = 1.f;
                if (slot.pageView->curling())
                {
                    scaleX -= _pageCurl->progress();
                }

                slot.pageView->renderShadow(-shadowX, -shadowX, scaleX);
            }
        }

        // render to staging texture applying horizontal blur
        constants::graphics()->setZWrite(g::enabled_state::disabled);
        _backRenderTarget->setRenderTarget(_backTexture2);
        _backRenderTarget->apply(constants::graphics());
        _backRenderer->render(
            1.f,
            0.f,
            m::point2d(1.f, 1.f),
            m::point2d(1.f, 1.f),
            m::point2d(0.f, 0.f),
            _backTexture1,
            post_processing_effect::horizontal_blur
            );

        // now a vertical blur
        _backRenderTarget->setRenderTarget(_backTexture1);
        _backRenderTarget->apply(constants::graphics());
        _backRenderer->render(
            1.f,
            0.f,
            m::point2d(1.f, 1.f),
            m::point2d(1.f, 1.f),
            m::point2d(0.f, 0.f),
            _backTexture2,
            post_processing_effect::vertical_blur
            );

        snapRenderTarget.apply(constants::graphics());

        constants::graphics()->clear(
            g::clear_targets::colorDepthAndStencil, 
            constants::general::backgroundPackedColor(),
            1.f, 0);

        // finally render to the backbuffer applying a darken filter
        _backRenderer->render(
            1.f,
            0.f,
            m::point2d(1.f, 1.f),
            m::point2d(1.f, 1.f),
            m::point2d(0.f, 0.f),
            _backTexture1,
            post_processing_effect::darken
            );

        constants::graphics()->setZWrite(g::enabled_state::enabled);
    }
    else
    {
        constants::graphics()->clear(
            g::clear_targets::colorDepthAndStencil, 
            constants::general::backgroundPackedColor(), 
            1.f, 0);
    }

    // render the scene graph
    std::shared_ptr<gs::iprepared_scene> preparedScene = _sceneGraph->prepareForRendering(constants::graphics());
    preparedScene->render(constants::graphics());

    if (withChrome)
    {
        // render references
        _sceneReferences->onRender();
        _inputReferences->onRender();

        // render the scroll bars
        this->updateScrollBars();
        _scrollBars->onRender();

        // render the performance stats on top of everything else
        constants::performanceService()->onRender();

        constants::memoryTrackingService()->onRender();

        if (_state == state::selectingWithLasso || _state == state::selectingWithLassoIdle)
        {
            this->renderLasso();
        }

        // finally, update the view info if we have to
        if (constants::references::showViewInfo())
        {
            this->renderViewInfo();
        }
    }
    constants::graphics()->popWorldMatrix();
}

void notebook::onTick()
{
    // If we must refresh the page set (likely because the user interaction stopped), then
    // do it now
    if (_refreshPageSet)
    {
        _pageSetDirty = true;
        this->updatePageSet(
            _sceneState.getCamera()->offsetXY(),
            _sceneState.getCamera()->focus()
            );
        _refreshPageSet = false;
    }

    // update animations
    b::float32 t = constants::general::currentTime();

    bool updatePagePositions = false;
    layout_id updateLayoutId;

    // Set page vertices to the curl shape, update the page normals for shadows, and 
    // set the alpha for curling.
    if (this->currentlyCurling())
    {
        if (_state == state::tapCurlTransition && t >= _openingEndTime)
        {
            _slots[_pageNumber].pageView->setAlpha(1.0f);   // un-hide it, so in grid view the page can be seen!
            if (_tapCurlTransitionForwards)
            {
                _pageNumber++;
            }
            _pageCurl->reset();
            updatePagePositions = true;
            updateLayoutId = _layoutId;
            _slots[_pageNumber].pageView->setCurling(false);
            this->goIdle();
        }

        if (_state == state::autoCurling && !_pageCurl->stillCurling())
        {
            DEBUG_CONSOLE_TRACE(L"auto curl finished");
            _slots[_pageNumber].pageView->setCurling(false);
            this->goIdle();
        }
        else
        {
            if (_state == state::tapCurlTransition)
            {
                const std::shared_ptr<paper_sheet_node> &pageNode = 
                    std::static_pointer_cast<paper_sheet_node>(_slots[_pageNumber].pageView->sceneNode());
                _pageCurl->startUserCurl(_startCurlAnimation.apply(t), (pageNode->height() - pageNode->offset()) / 2.0f);
            }

            if (_state != state::curlingBackwards)
            {
            _pageCurl->onRender();
            _slots[_pageNumber].pageView->setAlpha(_pageCurl->alphaValue());
            _slots[_pageNumber].pageView->setCurling(true);
        }
        }

        // Need to update the pageset to show possible page turn
        this->updatePageSet(
            this->currentLayout()->calculateCameraOffsetXY(_pageNumber), 
            this->currentLayout()->focus()
            );
    }

    // Handle any selections

    std::vector<size_t> selectionsToRemove;
    for (auto &iter : _selections)
    {
        size_t pageNumber = iter.first;
        page_selection &selection = iter.second;

        if (selection.safeToRemove())
        {
            selectionsToRemove.push_back(pageNumber);
        }

        updatePagePositions = true;
        updateLayoutId = layout_id::grid;
    }

    for (size_t pageNum : selectionsToRemove)
    {
        _selections.erase(pageNum);
    }

    if (_state == state::startingSelecting)
    {
        // start with N slots w/ N selections. As pages finish being unselected,
        // the selection items get removed, and when we're left with one selection,
        // that's the first item being selected.
        if (_selections.size() == 1)
        {
            _state = state::selectingPages;
        }
    }
    
    if (_state == state::finishedSelecting)
    {
        // not very performant?
        bool doneSelecting = true;
        for (auto &iter : _selections)
        {
            page_selection &selection = iter.second;
            if (!selection.isSelected())
            {
                doneSelecting = false;
            }
        }

        if (doneSelecting)
        {
            _selections.clear();
            this->goIdle();
        }
    }

    if (_state == state::finishedMovingSelectedPage)
    {
        _transitionProgress = _transitionAnimation.apply(t);
        if (t >= _transitionAnimationEndTime)
        {
            if (_layoutId == layout_id::grid)
            {
                _state = state::selectingPages;

                if (_selections.size() == 0)
                {
                    // subtlety: this happens from page deletion.
                    if (!_keepAppBarAfterResorting)
                    {
                        constants::uiService()->hideAppBar();
                    }
                    _keepAppBarAfterResorting = false;

                    this->goIdle();

                    // make sure to re-enabled these buttons
                    constants::uiService()->enableAppBarButton(a::app_bar_button_id::delete_page, true);
                    constants::uiService()->enableAppBarButton(a::app_bar_button_id::ok_page, true);
                }
            }
            else
            {
                this->goIdle();
            }
        }
        updatePagePositions = true;
        updateLayoutId = _layoutId;
    }

    if (_state == state::deletingPages)
    {
        _transitionProgress = _transitionAnimation.apply(t);
        if (t >= _transitionAnimationEndTime)
        {
            size_t currentPageNumberBeforeDeletion = _pageNumber;
            size_t firstPageNumberBeforeDeletion = _slots.begin()->first;

            // First remove them from the cache
            for (auto &iter : _selections)
            {
                size_t pageNumber = iter.first;
                page_selection selection = iter.second;
                if (selection.isSelected())
                {
                    size_t pageId = static_cast<size_t>(_notebookModel->getPageIdFromPageNumber(pageNumber));
                    if (_pageCache.contains(pageId))
                    {
                        _pageCache.remove(pageId);
                    }
                }
            }

            // Now remove the page state from the notebook, so there's no interference when
            // calling getPageIdFromPageNumber
            std::vector<size_t> pageNumbersRemoved;
            for (auto &iter : _selections)
            {
                size_t pageNumber = iter.first;
                page_selection &selection = iter.second;
                if (selection.isSelected())
                {
                    selection.makeDeselected();
                    pageNumbersRemoved.push_back(pageNumber);
                    if (_slots.find(pageNumber) != _slots.end())
                    {
                        _slots[pageNumber].pageView->model()->setLoadLevelAsync(model::page_level::page_deleted);
                        this->disableSlot(pageNumber);
                        _slots.erase(pageNumber);
                    }
                }
            }

            // Fix up slots
            page_slot_map slots;
            for (auto &iter : _slots)
            {
                size_t pageNumber = _notebookModel->getPageNumberFromPageId(iter.second.pageView->model()->storageId());
                slots[pageNumber] = iter.second;
            }
            _slots.clear();
            _slots = slots;

            // Finally fix up _selections
            for (size_t pageNumberRemoved : pageNumbersRemoved)
            {
                _selections.erase(pageNumberRemoved);
            }

            // Start loading the pages that are coming in from off screen. We just
            // need to change the pageSet for one frame.
            _pageSetDirty = true;

            // Be safe, but not stupid
            if (_slots.size() == 0)
            {
                _pageNumber = firstPageNumberBeforeDeletion;
            }
            else if (_slots.find(currentPageNumberBeforeDeletion) != _slots.end())
            {
                _pageNumber = currentPageNumberBeforeDeletion;
            }
            else
            {
                _pageNumber = _slots.begin()->first;
            }

            // If we're deleting a lot of pages, we should adjust the camera, especially
            // when deleting most of the screen of pages at the end
            _notebookCamera->moveToPage(
                this->currentLayout(), 
                _pageNumber,
                false,
                constants::animations::pageMoveDuration(),
                constants::animations::pageMoveAcceleration()
                );

            // reslot the remaining pages with an animation
            // set up transition animation			
            b::float32 t = constants::general::currentTime();
            _transitionProgress = 0.f;
            _transitionAnimationEndTime = t + constants::animations::pageMoveDuration();
            _transitionAnimation = b::tv_interpolation(
                t,
                _transitionAnimationEndTime,
                _transitionProgress,
                1 - _transitionProgress,
                constants::animations::pageMoveAcceleration().x,
                constants::animations::pageMoveAcceleration().y
                );

            _state = state::finishedMovingSelectedPage;
        }

        updatePagePositions = true;
        updateLayoutId = layout_id::grid;
    }
    
    if (_state == state::transitioningFromGrid || _state == state::transitioningToGrid)
    {
        _transitionProgress = _transitionAnimation.apply(t);

        if (t >= _transitionAnimationEndTime)
        {
            if (_state == state::transitioningFromGrid)
            {
                // If we were transitioning from grid now it's the time to do 
                // the actual layout update
                this->setLayoutIdInternal(_transitionNextLayoutId);
            }

            DEBUG_CONSOLE_TRACE(L"transition animation finished");

            if (_subState == sub_state::transitioningFromGridThenExpanding)
            {
                // if we are supposed to re-expand after we are done, do it now
                this->setStackExpansion(stack_state::expanded);

                _subState = sub_state::none;
            }
            else
            {
                this->goIdle();
            }
        }

        updatePagePositions = true;
        updateLayoutId = layout_id::grid;
    }
    else if (_state == state::movingSelectedPage)
    {
        updatePagePositions = true;
        updateLayoutId = layout_id::grid;
    }
    else if (_state == state::expanding || _state == state::collapsing)
    {
        _expansionProgress = _expansionAnimation.apply(t);

        if (_notebookCamera->scaleAnimation().isFinished())
        {
            // zoom back in mid way through the expansion
            b::float32 currentScale = _layoutData[_layoutId]->layout->scale();
            _notebookCamera->setScale(
                currentScale, 
                constants::animations::stackExpansionDuration() / 2,
                constants::animations::stackExpansionAcceleration()
                );
        }

        updatePagePositions = true;
        updateLayoutId = _layoutId;

        if (t >= _expansionAnimationEndTime)
        {
            if (_state == state::collapsing)
            {
                // update the stack state now
                this->setStackExpansionInternal(stack_state::stacked);

                // go ahead and update the page set to reflect the new stack changes
                this->updatePageSet(
                    this->currentLayout()->calculateCameraOffsetXY(_pageNumber), 
                    this->currentLayout()->focus()
                    );
            }

            DEBUG_CONSOLE_TRACE(L"expansion animation finished");

            if (_subState == sub_state::collapsingThenTransitioningToGrid)
            {
                this->setLayoutId(layout_id::grid);
                
                _subState = sub_state::none;
            }
            else if (_subState == sub_state::transitioningFromGridThenExpanding)
            {
                this->setLayoutId(layout_id::full);
                _subState = sub_state::none;
            }
            else
            {
                this->goIdle();
            }
        }
    }

    // TRACK: finish this onTick reorg
    _notebookCamera->onTick();

    // if we were transitioning, end the transition now
    if (_state == state::transitioningOther)
    {
        if (_notebookCamera->state() == notebook_camera_state::idle)
        {
            if (_subState == sub_state::transitioningOtherThenExpanding)
            {
                // if we are supposed to re-expand after we are done, do it now
                if (!this->setStackExpansion(stack_state::expanded))
                {
                    this->goIdle();
                }

                _subState = sub_state::none;
            }
            else
            {
                this->goIdle();
            }
        }
    }

    if (_state == state::starting)
    {
        // check to see if we need to open the cover (we open it half way through the starting animation)
        if (!_openingNotebook)
        {
            animation& a = _notebookCamera->scaleAnimation();
            if (t - a.start() > _notebookCamera->scaleAnimation().duration() / 2.f)
            {
                // already past half way through scale animation
                _openingEndTime = t + constants::animations::startingDuration();
                b::float32 pageMaxX = (_coverNode->width() - _coverNode->offset()) / 2.0f;
                _startCurlAnimation = b::linear_interpolation(t, _openingEndTime, pageMaxX, -pageMaxX);
                _pageCurl->attachPage(_coverNode, true, page_curl::paper_type::plasticCover);
                _coverNode->setCurling(true);
                _openingNotebook = true;
            }
        }
        else
        {
            _pageCurl->startUserCurl(_startCurlAnimation.apply(t), (_coverNode->height() - _coverNode->offset()) / 2.0f);
            _pageCurl->onRender();
            g::color c = _coverNode->getMaterial()->getDiffuseColor();
            c.a = _pageCurl->alphaValue();
            _coverNode->getMaterial()->setDiffuseColor(c);

            if (t >= _openingEndTime)
            {
                DEBUG_CONSOLE_TRACE(L"starting transition finished");
                _sceneGraph->removeChild(_coverNode);
                _coverNode->setCurling(false);
                _openingNotebook = false;
                // dipose the initial texture
                _coverNode = nullptr;
                _coverTexture = nullptr;

                // now expand the stack if we must
                if (constants::general::stackState() == stack_state::expanded)
                {
                    this->setStackExpansion(stack_state::expanded);
                }
                else
                {
                    this->goIdle();
                }
            }
        }
    }
    
    if (updatePagePositions)
    {
        // update the positions of all the pages
        for (auto &iter : _slots)
        {
            page_slot& slot = iter.second;

            // always use the grid layout for transition position calculations
            std::shared_ptr<ipage_layout> layout = _layoutData[updateLayoutId]->layout;

            this->updatePagePositionInternal(layout, slot);
        }
    }

    // see if we need to remove any slots, now it's the chance to do it
    std::vector<size_t> slotsToRemove;

    for (auto &iter : _slots)
    {
        page_slot& slot = iter.second;

        if (slot.isRemoved)
        {
            // make sure to unload the full page (the page view may opt to keep the thumbnail or even the full data around, this is just giving it the
            // hint that we won't need it from here anyway)
            slot.pageView->setLoadLevel(view::load_level::thumbnailOrNone);         
            
            slotsToRemove.push_back(slot.pageView->model()->pageNumber());
        }
        else
        {
            slot.pageView->onRender();
        }
    }

    for (const size_t pageNumber : slotsToRemove)
    {
        auto it = _slots.find(pageNumber);
        if (it != _slots.end())
        {
            DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"erasing slot %i") % pageNumber);
            _slots.erase(it);
        }
    }

    // manage idle time
    if (_idleTime.getElapsedMilliseconds() >= constants::general::thumbnailSaveInterval())
    {
        // take this chance to do something during idle time
        if (_saveThumbnails)
        {
            DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
                L"SaveThumbnailsAsync: calling from render loop (during idle time)"
                );

            this->saveThumbnailsAsync();
            _saveThumbnails = false;
        }
    }
    else
    {
        _saveThumbnails = true;
    }
}

void notebook::onRender()
{
    this->renderFrame(true); // withChrome
}

void notebook::updatePagePositionInternal(const std::shared_ptr<ipage_layout>& layout, page_slot& slot)
{
    const std::shared_ptr<paper_sheet_node> &pageNode = 
        std::static_pointer_cast<paper_sheet_node>(slot.pageView->sceneNode());
    size_t pageNumber = slot.pageView->model()->pageNumber();
    m::point3d position = layout->calculatePagePosition(pageNumber);
    m::point3d angle(0.f, 0.f, 0.f);
    bool deletingThisPage = false;
    switch (_state)
    {
    case state::movingSelectedPage:
        position = this->UPPIMovingSelectedPage(layout, slot);
        break;
    case state::finishedMovingSelectedPage:
        position = this->UPPIFinishedMovingSelectedPage(layout, slot);
        break;
    case state::deletingPages:
        if (_selections.find(pageNumber) != _selections.end())
        {
            deletingThisPage = true;
            this->UPPIDeletingSelectedPage(layout, slot, &position, &angle);			
        }
        break;
    case state::expanding:
    case state::collapsing:
    case state::transitioningFromGrid:
    case state::transitioningToGrid:
    case state::transitioningOther:
        this->UPPILayoutTransition(layout, slot, &position, &angle.z);
        break;
    }

    if (this->allowPageCurling())
    {
        // prevent z-fighting when curling... do this better too
        b::int32 ordinal = static_cast<b::int32>(pageNumber - _pageSet.minPage);
        position.z -= (ordinal * 0.02f);
    }

    // Move up or down z-axis if pages are selecting/deselecting
    // Also does page curling for selected pages if needed.
    // Subtlety: if pages are deleted, _selections is zero
    if (_selections.size() > 0) 
    {
        position = this->UPPISelectedPages(position, pageNumber, pageNode);
    }

    slot.pageView->setPosition(position);
    slot.pageView->setRotation(angle);

    // Darkness based on distance. Lighter if we are deleting the page
    if (deletingThisPage)
    {
        b::linear_interpolation li(0.f, constants::general::floorDistance(), 255, 210);
        b::uint32 color = static_cast<b::uint32>(li.apply(abs(position.z)));
        g::color c = g::packColor(255, color, color, color);
        slot.pageView->setAmbientColor(c);
    }
    else
    {
        b::linear_interpolation li(0.f, constants::general::floorDistance(), 255, 100);
        b::uint32 color = static_cast<b::uint32>(li.apply(abs(position.z)));
        g::color c = g::packColor(255, color, color, color);
        slot.pageView->setAmbientColor(c);
    }
}

m::point3d notebook::UPPIMovingSelectedPage(const std::shared_ptr<ipage_layout>& layout, page_slot& slot)
{
    BAJA_VERIFY(_state == state::movingSelectedPage);

    const std::shared_ptr<paper_sheet_node> &pageNode = 
        std::static_pointer_cast<paper_sheet_node>(slot.pageView->sceneNode());
    size_t pageNumber = slot.pageView->model()->pageNumber();
    m::point3d position = layout->calculatePagePosition(pageNumber); // position of slot page

    size_t selectedPageNumber;
    if (_state == state::movingSelectedPage)
    {
        selectedPageNumber = _selections.begin()->first;
    }

    m::point3d movingPosition; // current position of the page that's moving
    const std::shared_ptr<paper_sheet_node> &startPageNode = 
        std::static_pointer_cast<paper_sheet_node>(_slots[selectedPageNumber].pageView->sceneNode());
    startPageNode->getLocalPosition(&movingPosition);

    if (pageNumber == selectedPageNumber)
    {
        return movingPosition;
    }

    m::vector2d movingCenter = m::vector2d(movingPosition.x, movingPosition.y);
    movingCenter.x += constants::general::pageSlotWorldDimensions().x / 2.f;
    movingCenter.y -= constants::general::pageSlotWorldDimensions().y / 2.f;
    
    m::vector2d pageCenter = m::vector2d(position.x, position.y);
    pageCenter.x += constants::general::pageSlotWorldDimensions().x / 2.f;
    pageCenter.y -= constants::general::pageSlotWorldDimensions().y / 2.f;

    m::vector2d diff;
    pageCenter.subtract(movingCenter, &diff);
    b::float32 distance = diff.length();

    if (distance < 6.0)
    {
        // The distance between the centers of the two pages is less than a threshold.
        // Move the current page away from the moving page, in the direction of 'diff'.
        // The length of the diff is the threshold minus the actual distance, with a 
        // cap to make it look sensible.

        m::vector2d diffUnit;
        diff.normalize(&diffUnit);

        diffUnit.x *= min(6.0f - distance, 2.0f);
        diffUnit.y *= min(6.0f - distance, 2.0f);

        position.x += diffUnit.x / 2;
        position.y += diffUnit.y / 2;
    }

    return position;
}

m::point3d notebook::UPPIFinishedMovingSelectedPage(const std::shared_ptr<ipage_layout>& layout, page_slot& slot)
{
    BAJA_VERIFY(_state == state::finishedMovingSelectedPage);

    const std::shared_ptr<paper_sheet_node> &pageNode = 
        std::static_pointer_cast<paper_sheet_node>(slot.pageView->sceneNode());
    size_t pageNumber = slot.pageView->model()->pageNumber();
    m::point3d position = layout->calculatePagePosition(pageNumber);

    b::float32 progress = _transitionProgress;

    m::vector2d currentCenter = m::vector2d(position.x, position.y);
    currentCenter.x += constants::general::pageSlotWorldDimensions().x / 2.f;
    currentCenter.y -= constants::general::pageSlotWorldDimensions().y / 2.f;

    m::point3d currentPosition; 
    pageNode->getLocalPosition(&currentPosition);
    // this actually isn't linear, because currentPosition changes each time. I 
    // could store the original positions, but for now this actually looks pretty
    // nice.
    b::linear_interpolation_3d<m::point3d> li(0.f, 1.f, currentPosition, position);

    position = li.apply(progress);

    return position;
}

// Updates position & does curling if needed
m::point3d notebook::UPPISelectedPages(m::point3d position, size_t pageNumber, const std::shared_ptr<paper_sheet_node> &pageNode)
{
    BAJA_VERIFY(_selections.size() > 0);
    
    b::float32 deselectZ = -constants::general::floorDistance() / 2.0f;

    auto &iterSel = _selections.find(pageNumber);
    if (iterSel == _selections.end())
    {
        position.z = deselectZ;
    }
    else
    {
        page_selection &selection = iterSel->second;
        b::float32 progress = selection.transitionProgress();

        if (!selection.isSelected())
        {
            // Move pages that are being selected/deselected
            b::linear_interpolation li_z(0.0, 1.0, deselectZ, 0.0f);
            position.z = li_z.apply(progress);
        }

        // We want to curl pages when they are selected. Sadly we can't just
        // make use of the normal 'progress' transition variable, since
        // we need to curl the first selected page (which doesn't have its
        // shadow changed, nor position changed), and we need to uncurl the
        // last unselected page (also doesn't have its shadow or position
        // changed). I'm not sure I can make this logic any simpler. I might
        // be able to move it to the selection code, but then that gets more
        // complex.

        bool curl = false;
        bool decurl = false;
        if (_state == state::startingSelecting && selection.isSelected())
        {
            curl = true;
        }
        else if (_state == state::finishedSelecting && selection.isSelected())
        {
            decurl = true;
        }
        else if (_state == state::selectingPages && (selection.isSelected() || selection.safeToRemove()))
        {
            ;
        }
        else if (_state == state::selectingPages)
        {
            curl = true;
        }
        if (curl || decurl)
        {
            b::float32 maxY = (pageNode->height() - pageNode->offset()) / 2.0f;
            b::float32 flatX = (pageNode->width() - pageNode->offset()) / 2.0f;
            b::float32 curlX = flatX - 0.4f;
            if (decurl)
            {
                b::float32 t = flatX;
                flatX = curlX;
                curlX = t;
            }

            b::linear_interpolation li_curl(0.0, 1.0, flatX, curlX);

            // intentionally not calling pageNode->setCurling because we don't need transparency
            _pageCurl->attachPage(pageNode, true, page_curl::paper_type::looseLeaf);
            _pageCurl->startUserCurl(li_curl.apply(progress), maxY);
            _pageCurl->onRender();
        }
            
        selection.possiblyFinishTransition();
    }

    return position;
}

void notebook::UPPILayoutTransition(const std::shared_ptr<ipage_layout>& layout, page_slot& slot, m::point3d *position, b::float32 *angle)
{
    const std::shared_ptr<paper_sheet_node> &pageNode = 
        std::static_pointer_cast<paper_sheet_node>(slot.pageView->sceneNode());
    size_t pageNumber = slot.pageView->model()->pageNumber();

    bool applyProgress = false;
    b::float32 progress;

    if (_state == state::expanding || _state == state::collapsing)
    {
        progress = _expansionProgress;
        applyProgress = true;
    }
    else if (_state == state::transitioningFromGrid || _state == state::transitioningToGrid)
    {
        progress = _transitionProgress;
        applyProgress = true;
    }

    // interpolate the positions if needed
    b::int32 ordinal = static_cast<b::int32>(pageNumber - _pageSet.minPage);

    if (applyProgress)
    {
        // make sure the current page is on top, followed by the previous pages, then the next pages.
        // this transition still doesn't look so good going from expanded list to grid.
        if (pageNumber < _pageNumber)
        {
            // move all pages in front of the current page one position to the right
            // to make room for the current page to be on top
            ordinal++;
        }
        else if (pageNumber == _pageNumber)
        {
            ordinal = 0; // on top
        }

        // all pages start "stacked" in the same position as the current page
        // except with different z values
        m::point3d firstPosition = layout->calculatePagePosition(_pageNumber);
        b::float32 total = static_cast<b::float32>(_pageSet.maxPage - _pageSet.minPage + 1);

        // distribute the pages equally along the distance to the floor
        b::float32 distance = constants::general::floorDistance() / total;
        firstPosition.z = static_cast<b::float32>(-ordinal * distance);

        b::linear_interpolation_3d<m::point3d> li(0.f, 1.f, firstPosition, *position);
        *position = li.apply(progress);

        // now update the page angles
        if (pageNumber != _pageNumber)
        {
            // we need to spit the interpolation in two so it gets to the max angle at some middle point
            if (progress < slot.maxAngleProgress)
            {
                b::linear_interpolation li(0.f, slot.maxAngleProgress, 0.f, slot.transitionAngle);
                *angle = li.apply(progress);
            }
            else
            {
                b::linear_interpolation li(slot.maxAngleProgress, 1.f, slot.transitionAngle, 0.f);
                *angle = li.apply(progress);
            }
        }
    }

    // Prevent z-fighting
    position->z += ((ordinal+1) * -0.02f);
}

void notebook::UPPIDeletingSelectedPage(const std::shared_ptr<ipage_layout>& layout, page_slot& slot, m::point3d *position, m::point3d *angle)
{
    BAJA_VERIFY(_state == state::deletingPages);

    const std::shared_ptr<paper_sheet_node> &pageNode = 
        std::static_pointer_cast<paper_sheet_node>(slot.pageView->sceneNode());
    size_t pageNumber = slot.pageView->model()->pageNumber();
    m::point3d startPosition = layout->calculatePagePosition(pageNumber);
    b::float32 progress = _transitionProgress;

    m::point3d hell = startPosition;
    hell.z = hell.z - 50;
    hell.y = hell.y - 30;

    b::linear_interpolation_3d<m::point3d> li_p(0.f, 1.f, startPosition, hell);
    *position = li_p.apply(progress);

    b::linear_interpolation li_ax(0.f, 1.f, 0.0f, _randAngles[pageNumber].x);
    b::linear_interpolation li_ay(0.f, 1.f, 0.0f, _randAngles[pageNumber].y);
    b::linear_interpolation li_az(0.f, 1.f, 0.0f, _randAngles[pageNumber].z);

    angle->x = li_ax.apply(progress);
    angle->y = li_ay.apply(progress);
    angle->z = li_az.apply(progress);
}

bool notebook::setStackExpansion(const stack_state stackState)
{
    if (_layoutId == layout_id::grid)
    {
        return false; // not allowed for grid view
    }

    if (stackState == _stackState)
    {
        return false; // nothing to do
    }

    _state = (_stackState == stack_state::expanded) ? state::collapsing : state::expanding;

    // if we are currently expanded, don't change the stack bit in the layouts yet, this is because we first need
    // to do the collapsing animation and we need to remain in 'stacked' mode for that
    if (stackState == stack_state::expanded)
    {
        this->setStackExpansionInternal(stackState);
    }

    // move the camera to the middle of the current page
    _notebookCamera->moveToPage(
        this->currentLayout(), 
        _pageNumber,  
        false,
        constants::animations::pageMoveDuration(),
        constants::animations::pageMoveAcceleration()
        );

    // zoom out for half of the animation (we'll zoom back in later in onTic)
    b::float32 currentScale = _sceneState.getCamera()->scale();
    _notebookCamera->setScale(
        currentScale * 0.85f, 
        constants::animations::stackExpansionDuration() / 2,
        constants::animations::stackExpansionAcceleration()
        );

    // set up the animation
    _expansionProgress = (stackState == stack_state::stacked) ? 1.f : 0.f;
    b::float32 t = constants::general::currentTime();
    _expansionAnimationEndTime = t + constants::animations::stackExpansionDuration();

    _expansionAnimation = b::tv_interpolation(
        t,
        _expansionAnimationEndTime,
        _expansionProgress,
        1 - _expansionProgress,
        constants::animations::stackExpansionAcceleration().x,
        constants::animations::stackExpansionAcceleration().y
        );

    // go ahead and update the page set to reflect the new stack changes
    this->updatePageSet(
        this->currentLayout()->calculateCameraOffsetXY(_pageNumber), 
        this->currentLayout()->focus()
        );

    // add a bit of a random tilt to make the transition more interesting
    // update the positions of all the pages
    std::for_each(
        _slots.begin(), _slots.end(),
        [&](std::pair<const size_t, page_slot>& pair)
    {
        page_slot& slot = pair.second;

        slot.transitionAngle = b::rand(-10.f, 10.f);
        slot.maxAngleProgress = b::rand(0.3f, 0.8f);
    });

    return true;
}

void notebook::setStackExpansionInternal(const stack_state stackState)
{
    // set the stacked bit in both of the stackable layouts we have (so the stacked state is kept between transitions)
    std::shared_ptr<list_page_layout> layout = std::dynamic_pointer_cast<list_page_layout>(_layoutData[layout_id::full]->layout);
    layout->setStacked((stackState == stack_state::stacked) ? true : false);
    layout = std::dynamic_pointer_cast<list_page_layout>(_layoutData[layout_id::fitToWidth]->layout);
    layout->setStacked((stackState == stack_state::stacked) ? true : false);

    // Set the current page as reference so it doesn't move from the place it is in
    this->currentLayout()->setReference(_pageNumber, _slots[_pageNumber].pageView->position()); 
    // TRACK: there're cases where this page is not loaded yet (pageView is empty).  make sure to time everything right.  use locks

    _stackState = stackState;

    // also update the setting value
    constants::settings()->setEnum((int)constants::general::id::stackState, static_cast<int>(_finalStackState));
}

void notebook::setLayoutIdWithStack(const layout_id nextLayoutId)
{
    // handle maintaining stack expansion
    // full -> grid 
    if (_layoutId == layout_id::full && 
        nextLayoutId == layout_id::grid && 
        _stackState == stack_state::expanded
        )
    {
        // expanded and going to grid, first stack them up, then do the zoom
        this->setStackExpansion(stack_state::stacked);

        // remember the fact that we are supposed to be expanded
        _subState = sub_state::collapsingThenTransitioningToGrid;
    
    // grid -> full
    }
    else if (_layoutId == layout_id::grid && 
        nextLayoutId == layout_id::full && 
        _finalStackState == stack_state::expanded)
    {
        // expanded and coming from grid, first do the transition, then re-expand
        _subState = sub_state::transitioningFromGridThenExpanding;

        this->setLayoutId(nextLayoutId);
    }
    // fit to width -> full
    else if (_layoutId == layout_id::fitToWidth && 
        nextLayoutId == layout_id::full &&
        _finalStackState == stack_state::expanded)
    {
        _subState = sub_state::transitioningOtherThenExpanding;

        this->setLayoutId(nextLayoutId);
    }
    else
    {
        this->setLayoutId(nextLayoutId);
    }
}

void notebook::setLayoutId(const layout_id layoutId)
{
    if (layoutId == _layoutId)
    {
        return; // no change
    }

    // check that this is an allowed layout transition and set the new state accordingly
    if (_layoutId == layout_id::full || _layoutId == layout_id::fitToWidth)
    {
        if (layoutId == layout_id::fitToWidth || layoutId == layout_id::full)
        {
            _state = state::transitioningOther;
            this->setLayoutIdInternal(layoutId);

            // update if we are expanded and going back to full, so the pages show up
            if (layoutId == layout_id::full && _stackState == stack_state::expanded)
            {
                // update the page set
                this->updatePageSet(
                    _layoutData[layoutId]->layout->calculateCameraOffsetXY(_pageNumber), 
                    _layoutData[layoutId]->layout->focus()
                    );
            }
        }
        else if (layoutId == layout_id::grid)
        {
            _state = state::transitioningToGrid;
            
            _transitionProgress = 0.f;

            // we can switch layouts now since the transition will happen in the grid layout
            this->setLayoutIdInternal(layoutId);

            // update the page set
            this->updatePageSet(
                _layoutData[layoutId]->layout->calculateCameraOffsetXY(_pageNumber), 
                _layoutData[layoutId]->layout->focus()
                );

            // since we are transitioning to grid, go over each page and add a bit of a random tilt to make the transition more interesting
            // update the positions of all the pages
            std::for_each(
                _slots.begin(), _slots.end(),
                [&](std::pair<const size_t, page_slot>& pair)
            {
                page_slot& slot = pair.second;

                slot.transitionAngle = b::rand(-10.f, 10.f);
                slot.maxAngleProgress = b::rand(0.3f, 0.8f);
            });
        }
    }
    else if (_layoutId == layout_id::grid)
    {
        // set reference for new layout
        _layoutData[layoutId]->layout->setReference(_pageNumber, _slots[_pageNumber].pageView->position());

        _state = state::transitioningFromGrid;
        _transitionNextLayoutId = layoutId;

        _transitionProgress = 1.f;

        // in this case we are not setting the layout id yet, we'll wait until the transition is completed and then set the new layout id
        // this is because we need to stay in the grid layout to perform the animation (collapsing all the pages into a stack)
    }
    else if (_layoutId == layout_id::none)
    {
        this->setLayoutIdInternal(layoutId);

        // then apply the starting scale (no animation)
        _notebookCamera->setScale(_startingScale);

        // now animate it back to the normal position
        _notebookCamera->setScale(
            _layoutData[_layoutId]->layout->scale(), 
            constants::animations::startingDuration(),
            constants::animations::startingAcceleration()
            );

        _state = state::starting; 
    }

    if (_state == state::none)
    {
        BAJA_THROW(std::logic_error("invalid layout transition"));
    }

    // set up animations
    b::float32 t = constants::general::currentTime();

    // set up transition animation
    if ((_state == state::transitioningFromGrid) || (_state == state::transitioningToGrid))
    {
        _transitionAnimationEndTime = t + constants::animations::transitionDuration();

        // set up the layout transition
        _transitionAnimation = b::tv_interpolation(
            t,
            _transitionAnimationEndTime,
            _transitionProgress,
            1 - _transitionProgress,
            constants::animations::transitionAcceleration().x,
            constants::animations::transitionAcceleration().y
            );
    }

    // we don't want inertia for the fit-to-width mode since we use a different set of gestures for page turning
    if (layoutId != layout_id::fitToWidth)
    {
        constants::shellService()->enableInertia(constants::general::inertialDeceleration());
    }
    else
    {
        constants::shellService()->disableInertia();
    }

    // always "re-move" to the current page in the new layout
    // this will make sure any camera updates happen
    if (_state != state::starting)
    {
        _notebookCamera->moveToPage(
            _layoutData[layoutId]->layout, 
            _pageNumber, 
            false, 
            constants::animations::pageMoveDuration(),
            constants::animations::pageMoveAcceleration()
            );
    }

    _pageSetDirty = true;
}

layout_id notebook::getNextLayout(const zoom_direction direction)
{
    layout_id nextLayoutId = _layoutId;

    if (_layoutId == layout_id::fitToWidth && direction == zoom_direction::out)
    {
        nextLayoutId = layout_id::full;
    }
    else if (_layoutId == layout_id::full)
    {
        nextLayoutId = (direction == zoom_direction::out) ? layout_id::grid : layout_id::fitToWidth;
    }
    else if (_layoutId == layout_id::grid && direction == zoom_direction::in)
    {
        nextLayoutId = layout_id::full;
    }

    return nextLayoutId;
}

void notebook::setLayoutIdInternal(const layout_id layoutId)
{
    // set up the layout we'll be transitioning to
    std::shared_ptr<ipage_layout> newLayout = _layoutData[layoutId]->layout;

    size_t referencePage = 0;
    m::point3d referencePosition = m::point3d(0.f, 0.f, 0.f);
    
    if (_layoutId != layout_id::none)
    {
       // not first time we set a layout, so use new origin
        referencePage = _pageNumber;
        referencePosition = this->currentLayout()->calculatePagePosition(referencePage);
    }

    // set reference for new layout
    newLayout->setReference(referencePage, referencePosition);

    _layoutId = layoutId;

    // also update the setting value
    constants::settings()->setEnum((int)constants::general::id::zoomLevel, static_cast<int>(_layoutId));
}

void notebook::updatePageNumberInternal()
{
    size_t newPageNumber;
    page_set newPageSet(NO_PAGE, NO_PAGE);

    this->currentLayout()->calculatePageSet(
        _sceneState.getCamera()->offsetXY(),
        _sceneState.getCamera()->focus(),
        _pageSet,
        _pageNumber,
        &newPageSet,
        &newPageNumber
        );

    _pageNumber = newPageNumber;
}

void notebook::onPagePurged(const size_t& pageNumber, std::shared_ptr<ipage>& pageView)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"page %i purged from the cache") % pageNumber);

    pageView->setLoadLevel(view::load_level::none);         
}

// handler for model::inotebook_model_observer::onPageCreated()
void notebook::onPageModelCreated(const std::shared_ptr<model::ipage_model>& model)
{
    ACQUIRE_VIEW_LOCK;

    // TRACK: this needs to be a bit more sophisticated and figure out if the page will be created or not
    // based on the space available in the viewport (the current pageset doesn't suffice)
    size_t pageNumber = model->pageNumber();
    this->enableSlot(pageNumber);

    bool appendingToGrid = false;
    bool curling = this->allowPageCurling();
    bool expanded = (_layoutId == layout_id::fitToWidth || _layoutId == layout_id::full) && (_stackState == stack_state::expanded);
    bool grid = _layoutId == layout_id::grid;
    BAJA_VERIFY(curling || expanded || grid);

    // Here's how we add a new page to the notebook:
    // 1) Add the new page to the end of the notebook model. 
    // 2) Move the new page from oldPageNumber to newPageNumber
    //     * Update the view data
    //     * Setup an animation
    //
    // Depending on what view we're in (curling, expanded, grid)
    // newPageNumber will be different, and the animation will 
    // be different.

    size_t oldPageNumber = _notebookModel->pageCount()-1;
    size_t newPageNumber;
    if (curling)
    {
        newPageNumber = static_cast<b::uint32>(_pageNumber + 1);
    }
    else if (expanded)
    {
        newPageNumber = static_cast<b::uint32>(_pageNumber);
    }
    else if (grid)
    {
        size_t dummy;
        page_set newPageSet(NO_PAGE, NO_PAGE);
        this->currentLayout()->calculatePageSet(
            _sceneState.getCamera()->offsetXY(),
            _sceneState.getCamera()->focus(),
            _pageSet,
            _pageNumber,
            &newPageSet,
            &dummy
            );

        // Add the new page AFTER the last page in the current page set
        // unless the screen is full, then add it BEFORE the last page
        // in the current pageset.

        newPageNumber = _pageSet.maxPage + 1;

        if (_pageSet.maxPage == newPageSet.maxPage)
        {
            newPageNumber--;
        }
        else
        {
            appendingToGrid = true;
        }
    }
    size_t movePageAfter = newPageNumber - 1;
    bool addingAtBeginning = newPageNumber == 0;

    // Update data structures to move oldPageNumber to newPageNumber

    if (oldPageNumber != newPageNumber)
    {
        // why so much code? well, the pagecache & the slots depend on the page numbers, which are changing
        // here, and must be updated. This code is similar to the code to re-sort pages when moving in grid
        // view, but two things are different:
        // 1) If we are in stacked mode, we know there are only a few slot entries
        // 2) oldPageNumber > newPageNumber, because createPageModelAsync always adds at the end.
        _notebookModel->movePage(oldPageNumber, newPageNumber);
        
        // update slots

        if (curling)
        {
            BAJA_VERIFY(_slots[_pageNumber].pageView && _slots[_pageNumber+1].pageView && _slots[oldPageNumber].pageView);
        }
        
        page_slot_map slots;
        if (curling)
        {
            // Hack: hide the old page underneath to prevent a flicker. goddamn flickers.
            this->disableSlot(newPageNumber);
        }
        slots[newPageNumber] = _slots[oldPageNumber];
        _slots.erase(oldPageNumber);
        for (auto &iter : _slots)
        {
            b::uint32 pageNumber = static_cast<b::uint32>(iter.first);
            page_slot &slot = iter.second;
            b::uint32 insertionNumber;
            if (addingAtBeginning || pageNumber > movePageAfter)
            {
                insertionNumber = pageNumber + 1;
            }
            else
            {
                insertionNumber = pageNumber;
            }
            BAJA_VERIFY(slots.find(insertionNumber) == slots.end());
            slots[insertionNumber] = slot;
        }
        
        _slots.clear();
        _slots = slots;

        // this is a bit of a hack (but this whole method is already...)
        // this will make sure the new page's z-position will be recalculated the right way after being moved
        this->enableSlot(newPageNumber);
    }

    // Animate
    if (curling)
    {
        this->autoCurlThisPageAway(true);
    }
    else if (expanded || grid)
    {
        // Pages are already renumbered, but they need to slide into place. Reuse the
        // finishedMovingSelectedPages state. Have the new page start at startPosition
        // and slide where it's supposed to end up.

        this->enableSlot(newPageNumber);

        // Tell the notebook we are showing N+1 pages. This way, once the notebook goes 
        // back idle and we show N pages, the last page is removed from _slots.
        
        _pageSet.maxPage++;

        m::point3d startPosition;

        if (expanded)
        {
            const std::shared_ptr<paper_sheet_node> &pageNodeCur = 
                std::static_pointer_cast<paper_sheet_node>(_slots[_pageNumber].pageView->sceneNode());
            pageNodeCur->getLocalPosition(&startPosition);
        
            startPosition.x = startPosition.x - 10;
            startPosition.y = startPosition.y - 20;
        }
        else if (grid)
        {
            const std::shared_ptr<paper_sheet_node> &pageNodeCur = 
                std::static_pointer_cast<paper_sheet_node>(_slots[_pageSet.minPage].pageView->sceneNode());
            pageNodeCur->getLocalPosition(&startPosition);
        
            startPosition.x = startPosition.x - 10;
            startPosition.y = startPosition.y + 10;
        }

        const std::shared_ptr<paper_sheet_node> &pageNode = 
            std::static_pointer_cast<paper_sheet_node>(_slots[newPageNumber].pageView->sceneNode());
        pageNode->setPosition(startPosition);

        // set up transition animation

        b::float32 t = constants::general::currentTime();
        _transitionProgress = 0.f;
        _transitionAnimationEndTime = t + constants::animations::pageMoveDuration();
        _transitionAnimation = b::tv_interpolation(
            t,
            _transitionAnimationEndTime,
            _transitionProgress,
            1 - _transitionProgress,
            constants::animations::pageMoveAcceleration().x,
            constants::animations::pageMoveAcceleration().y
            );

        _state = state::finishedMovingSelectedPage;
    }

    _keepAppBarAfterResorting = true;
}

// handler for model::inotebook_model_observer::onPageLoadLevelSet()
void notebook::onPageLoadLevelSet(const std::shared_ptr<model::ipage_model>& page, const model::page_level currentLevel)
{
    ACQUIRE_VIEW_LOCK;

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"** notebook::onPageLoadLevelSet() : page id = %i, page number  = %i : level %i : no-op") %
        page->storageId() %
        page->pageNumber() %
        static_cast<b::uint32>(currentLevel)
        );
}

void notebook::updatePageSet(const m::vector2d& offsetXY, const m::vector2d focus)
{
    page_set newPageSet(NO_PAGE, NO_PAGE);
    size_t newPageNumber;

    // Determine the newPageSet to render from the layout

    this->currentLayout()->calculatePageSet(
        offsetXY,
        focus,
        _pageSet,
        _pageNumber,
        &newPageSet,
        &newPageNumber
        );
    
    if (_layoutId != layout_id::grid && _stackState == stack_state::stacked && _state != state::curlingBackwards)
    {
        if (newPageSet.maxPage < _notebookModel->pageCount() - 1)
        {
            newPageSet.maxPage++;
        }
    }

    if (_state == state::curlingBackwards)
    {
        BAJA_VERIFY(_pageNumber > 0);			
        newPageSet.minPage--;
    }

    if (this->currentlyCurling() && _state != state::curlingBackwards)
    {
        if (_pageNumber + 1 < _notebookModel->pageCount())
        {
            if (_pageCurl->shouldHideThisPage())
            {
                newPageSet.minPage++;                           // don't render current page
                _slots[_pageNumber].pageView->setAlpha(1.0f);   // un-hide it, so in grid view the page can be seen!
                _pageNumber = _pageNumber + 1;
                _pageCurl->reset();
                _slots[_pageNumber].pageView->setCurling(false);
                this->goIdle();
            }
        }
    }

    // Enable & disable the slots by comparing newPageSet with oldPageSet
    
    if (_pageSet != newPageSet || _pageSetDirty)
    {
        this->updatePageSetInternal(newPageSet);
    }
    
    if (_state == state::curlingBackwards)
    {
        BAJA_VERIFY(this->allowPageCurling());

        // the prev page is now the active page
        _pageNumber = _pageNumber - 1;
        _state = state::curling;
        _pageCurl->attachPage(_slots[_pageNumber].pageView, false, page_curl::paper_type::looseLeaf);
        _pageCurl->startUserCurl(_startCurlBackwardsPoint.x, 0);    // curling starts off fully curled
        _slots[_pageNumber].pageView->setAlpha(0.0f);               // page shows up transparent
    }

    if (this->currentlyCurling())
    {
        _slots[_pageNumber].pageView->setCurling(true);             // hide the shadow
    }
    
    _pageSetDirty = false;
}

void notebook::updatePageSetInternal(page_set &newPageSet)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"page set changed: %s (old set: %s)") % newPageSet.str() % _pageSet.str());

    // update the page set since some calls to enableSlot will want to know the new page set

    page_set oldPageSet = _pageSet;
    _pageSet = newPageSet;

    size_t enableCount = 0;
    size_t disableCount = 0;

    BAJA_VERIFY((oldPageSet.minPage != NO_PAGE) || (oldPageSet.minPage == oldPageSet.maxPage));
    BAJA_VERIFY((newPageSet.minPage != NO_PAGE) || (newPageSet.minPage == newPageSet.maxPage));

    // if we are in stacked mode, we want to preload the next page but fully load the current one
    // treat that as a special case
    size_t pageToPreload = NO_PAGE;
    if ((_stackState == stack_state::stacked) &&
        (newPageSet.maxPage - newPageSet.minPage == 1)) // there're two pages
    {
        // we'll want to preload the page behind the current one
        pageToPreload = newPageSet.maxPage;
    }

    if (oldPageSet.minPage == NO_PAGE)
    {
        // current set is empty
        if (newPageSet.minPage == NO_PAGE)
        {
            // new set is empty too, nothing to do
        }
        else
        {
            // just enable everything in the new set
            for (size_t i = newPageSet.minPage; 
                i <= newPageSet.maxPage && i < _notebookModel->pageCount(); 
                i++)
            {
                this->enableSlot(i, i == pageToPreload);
                enableCount++;
            }
        }
    }
    else // current set is not empty
    {
        if (newPageSet.minPage == NO_PAGE)
        {
            // new set is empty, just disable all the existing pages
            for (size_t i = oldPageSet.minPage; i <= oldPageSet.maxPage; i++)
            {
                this->disableSlot(i);
                disableCount++;
            }
        }
        else // neither set is empty, so do the intersection
        {
            for (size_t i = oldPageSet.minPage; i <= oldPageSet.maxPage; i++)
            {
                if (i >= newPageSet.minPage && i <= newPageSet.maxPage)
                {
                    continue;
                }
                else
                {
                    this->disableSlot(i);
                    disableCount++;
                }
            }

            for (size_t i = newPageSet.minPage; i <= newPageSet.maxPage && i < _notebookModel->pageCount(); i++)
            {
                this->enableSlot(i, i == pageToPreload);
                enableCount++;
            }
        }
    }
        
    if (pageToPreload == NO_PAGE)
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"new enabled count: %i, disabled count: %i, preloaded: none") % enableCount % disableCount)
    }
    else
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"new enabled count: %i, disabled count: %i, preloaded: %s") % enableCount % disableCount % pageToPreload)
    }
}

void notebook::enableSlot(const size_t& pageNumber, const bool preload)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"entering notebook::enableSlot for page %i") % pageNumber);

    std::shared_ptr<ipage> pageView;

    bool loaded = true;
    std::shared_ptr<model::ipage_model> pageModel;

    // not loaded yet ?
    size_t pageId = _notebookModel->getPageIdFromPageNumber(pageNumber);
    if (!_pageCache.tryGet(pageId, &pageView))
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"page %i not found in cache, creating new one") % pageNumber);

        pageModel = _notebookModel->getPage(static_cast<b::uint32>(pageNumber));
        pageView = createPage(
            pageModel, 
            std::static_pointer_cast<a::iinput_capture>(shared_from_this())
            );

        // add to cache
        _pageCache.add(pageId, pageView, true);
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"adding page %i to the cache") % pageNumber);
    }
    else
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"found page: %i in the cache") % pageNumber);
    }

    pageModel = pageView->model();

    // create a slot for the page if we don't have one
    auto it = _slots.find(pageNumber);
    if (it == _slots.end())
    {
        // create new one
        page_slot slot;
        slot.pageView = pageView;
        slot.observerCookie = slot.pageView->addObserver(std::dynamic_pointer_cast<ipage_observer>(shared_from_this()));
        slot.pageView->setVisible(true);
        this->updatePagePositionInternal(this->currentLayout(), slot);
        _sceneGraph->addChild(slot.pageView->sceneNode());

        _slots.insert(std::make_pair(pageNumber, slot));
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"enabling NEW slot for page %i") % pageNumber);
    }
    else
    {
        // if the slot is marked for removal undo that now
        if (it->second.isRemoved)
        {
            it->second.pageView->setVisible(true);
            _sceneGraph->addChild(it->second.pageView->sceneNode());
            it->second.observerCookie = it->second.pageView->addObserver(std::dynamic_pointer_cast<ipage_observer>(shared_from_this()));
            it->second.isRemoved = false;
            DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"slot for page %i was marked for removal, reverting") % pageNumber);
        }
        else
        {
            this->updatePagePositionInternal(this->currentLayout(), it->second);
            DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"enabling EXISTING slot for page%i") % pageNumber);
        }
    }

    // set the right load level for the new page
    // if the state of the view is idle, then go ahead and load whatever we must full or thumbnail)
    // however in any other state (panning, transitioning etc), always go to thumbnail, unless we are in full (in which case, leave it alone)
    load_level loadLevel = load_level::none;
    if (_state == state::idle)
    {
        loadLevel = preload ? load_level::thumbnail : _layoutData[_layoutId]->loadLevel;
    }
    else
    {
        loadLevel = view::load_level::thumbnailOrFull;
    }

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"starting load of page %i at level %i (layout id: %i)") %
        pageNumber %
        static_cast<b::uint32>(_layoutData[_layoutId]->loadLevel) %
        static_cast<b::uint32>(_layoutId)
        );
    pageView->setLoadLevel(loadLevel);
    pageView->setCurling(false);

    // If the user selects a page, the pageView will have its selected bit set.
    // If the page is evicted from the cache (say, by the user panning far out
    // into the distance) and then re-enabled, the selected bit won't be set.
    // In this case, we need to re-curl the page.
    //
    // Similarly, if the user has a bunch of pages selected, pans away,
    // then clicks the 'Done' button to select all pages, we need to ensure
    // the page is flat when it is re-enabled

    bool forceMeFlat = false;
    bool forceMeCurl = false;

    if (_selections.size() > 0)
    {
        auto iter = _selections.find(pageNumber);
        if (   iter != _selections.end()    // page is selected 
            && iter->second.isSelected()
            && !pageView->selected())       // but pageView doesn't know it
        {
            forceMeCurl = true;
            pageView->setSelected(true);
        }
    }

    if (   _selections.size() == 0  // nothing is selected (deselectAll finished)
        && pageView->selected())    // but the pageView thinks it should be
    {
        forceMeFlat = true;
        pageView->setSelected(false);
    }

    if (forceMeFlat || forceMeCurl)
    {
        const std::shared_ptr<paper_sheet_node> &pageNode = 
            std::static_pointer_cast<paper_sheet_node>(pageView->sceneNode());
        b::float32 maxY = (pageNode->height() - pageNode->offset()) / 2.0f;
        b::float32 flatX = (pageNode->width() - pageNode->offset()) / 2.0f;
        b::float32 curlX = flatX - 0.4f;

        pageView->setCurling(true);        
        _pageCurl->attachPage(pageNode, true, page_curl::paper_type::looseLeaf);
        _pageCurl->startUserCurl(forceMeFlat ? flatX : curlX, maxY);
        _pageCurl->onRender();
        pageView->setCurling(false);
}
}

void notebook::disableSlot(const size_t& pageNumber)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"entering notebook::disableSlot for page %i") % pageNumber);

    auto &iter = _selections.find(pageNumber);
    if (iter != _selections.end() && iter->second.isSelected())
    {
        // Don't disable slots for pages that are selected, since
        // they could be deleted, and right now the only way to
        // get to the page model is through the pageView, through
        // the _slots struture.
        return;
    }

    auto it = _slots.find(pageNumber);
    if (it != _slots.end())
    {
        page_slot& slot = it->second;

        slot.pageView->setVisible(false);
        _sceneGraph->removeChild(slot.pageView->sceneNode());
        slot.pageView->removeObserver(slot.observerCookie);
        slot.observerCookie = -1;
        slot.isRemoved = true; // mark for deletion
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"removed slot for for page %i") % pageNumber);
    }
    else
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_SLOTS, boost::wformat(L"couldn't find slot for for page %i, so nothing to remove") % pageNumber);
    }
}

bool notebook::allowPageCurling()
{
    return (_stackState == stack_state::stacked)
        && (_layoutId == layout_id::full || _layoutId == layout_id::fitToWidth);
}

bool notebook::currentlyCurling()
{
    switch (_state)
    {
    case state::autoCurling:
    case state::curling:
    case state::curlingBackwards:
    case state::tapCurlTransition:
        BAJA_VERIFY(this->allowPageCurling());
        return true; 
    default:
        return false;
    }	
}

void notebook::deselectAll()
{
    b::float32 pageSelectDuration = constants::animations::pageSelectDuration();
    b::float32 pageCountY = (b::float32) constants::general::gridLayoutPageCountY();
    size_t firstPageShownNumber = _slots.begin()->first;

    for (auto &iter : _slots)
    {
        size_t pageNumber = iter.first;
        page_slot &slot = iter.second;
        page_selection &selection = _selections[pageNumber];
        b::float32 duration = pageSelectDuration;

        if (selection.isSelected())
        {
            selection.startTransition(duration, true);
            selection.makeSelected();
        }
        else
        {
            duration += (pageNumber - firstPageShownNumber) / pageCountY * 80.0f;
            selection.startTransition(duration, true);
        }

        slot.pageView->setSelected(false);
    }

    constants::uiService()->hideAppBar();
    _state = state::finishedSelecting;
}

void notebook::cleanup()
{
    b::services::set<notebook>(std::shared_ptr<icamera_service>());

    // clear the page cache (this will free up all the pages)
    _pageCache.clear();

    _slots.clear();

    _sceneReferences = nullptr;
    _inputReferences = nullptr;
    _sceneGraph = nullptr;
    constants::settings()->removeObserver(_settingsObserverCookie);
    _settingsObserverCookie = -1;
}

void notebook::renderViewInfo()
{
    std::shared_ptr<g::iengine> graphics = constants::graphics();

    // calculate current camera scale
    b::float32 windowWorldWidth = constants::shellService()->windowSize().x / constants::shellService()->logicalDpi();
    b::float32 scale = windowWorldWidth / _sceneState.getCamera()->focus().x;

    boost::wformat fmt;
    if (_pageNumber == NO_PAGE)
    {
        fmt = boost::wformat(L"page --, %s (%0.2f%%)") % 
            _layoutData[_layoutId]->name %
            (100.f * scale);
    }
    else
    {
        fmt = boost::wformat(L"page %i, %s (%0.2f%%)") % 
            _pageNumber %
            _layoutData[_layoutId]->name %
            (100.f * scale);
    }

    std::wstring ws = fmt.str();

    g::viewport vp;
    graphics->getViewport(&vp);

    // measure the text's rect
    m::rectangle textRect;
    _font->measureText(
        ws.c_str(),
        g::horizontal_alignment::right,
        g::vertical_alignment::top,
        false,
        &textRect
        );

    textRect.top += 42;
    textRect.bottom += 42;
    textRect.left += vp.width - 10;
    textRect.right += vp.width - 10;

    // draw backdrop rectangle
    m::rectangle backdropRect(textRect);
    backdropRect.top -= 3;
    backdropRect.bottom += 1;
    backdropRect.left -= 4;
    backdropRect.right += 2;
            
    std::shared_ptr<g::ieffect2> effect;
    effect = graphics->getStandardEffect(g::standard_effect::screenColored);

    effect->begin(graphics);
    graphics->setZBuffer(g::enabled_state::disabled);
    graphics->setCulling(g::cull_state::none);

    graphics->beginScreenSpace(g::primitive_type::triangleStrip);

    graphics->setColor(g::packColor(128, 0, 0, 0));

    m::point4d pos1(backdropRect.left-0.5f, backdropRect.top-0.5f, 1.0f);
    graphics->setScreenSpaceVertex(pos1);

    m::point4d pos2(backdropRect.right-0.5f, backdropRect.top-0.5f, 1.0f);
    graphics->setScreenSpaceVertex(pos2);

    graphics->setColor(g::packColor(128, 0, 0, 0));

    m::point4d pos4(backdropRect.left-0.5f, backdropRect.bottom-0.5f, 1.0f);
    graphics->setScreenSpaceVertex(pos4);

    m::point4d pos3(backdropRect.right-0.5f, backdropRect.bottom-0.5f, 1.0f);
    graphics->setScreenSpaceVertex(pos3);

    graphics->end();

    effect->end(graphics);

    // draw the fps text

    static const g::packed_color textColor = g::packColor(255,198,198,198);
    textRect.right -= 2;
    textRect.top -= 2;
    _font->renderText(
        graphics,
        ws.c_str(),
        textColor,
        g::horizontal_alignment::left,
        g::vertical_alignment::top,
        &textRect
        );
}

void notebook::goIdle()
{
	DEBUG_CONSOLE_TRACE(boost::wformat(L"going idle from %i") % static_cast<int>(_state));
    _state = state::idle;

    // hide scrollbars
    _scrollBars->setVisibility(false);

    // make sure we refresh the page set in the next frame
    // this is to make sure we set the right load level (full or thumb) after we go idle
    // since we will prevent loading full if the user is moving around or during transitions
    _refreshPageSet = true;

    // make sure to re-enabled these buttons in case we disabled them due to a deletion
    constants::uiService()->enableAppBarButton(a::app_bar_button_id::delete_page, true);
    constants::uiService()->enableAppBarButton(a::app_bar_button_id::ok_page, true);
}

void notebook::generateTileImageAsync(std::function<void(const bool, const std::wstring&)> f)
{
    std::shared_ptr<ipage> pageView;
    
    if (_slots.end() != _slots.find(_pageNumber))
    {
        pageView = _slots[_pageNumber].pageView;
    }

    if (!pageView)
    {
        return;
    }

    b::float32 aspectRatio = constants::general::pagePixelDimensions().y / constants::general::pagePixelDimensions().x;

    m::point2d dimensions(
        constants::general::tileFileWidth(),
        constants::general::tileFileWidth() * aspectRatio
        );

    std::shared_ptr<imemory_blob> blob = pageView->generateImage(&dimensions);
    
    if (blob)
    {
        winrt::saveImageAsync(
            constants::general::tileFileName(),
            blob, 
            default_folder::app, 
            [=](std::shared_ptr<imemory_blob>&)
            {
                f(false, constants::general::tileFileName());
            });
    }
}

bool notebook::saveThumbnailsAsync()
{
    size_t thumbnailSaveCount = _slots.size();
    for (auto &iter : _slots)
    {
        if (!iter.second.pageView->saveThumbnailAsync())
        {
            thumbnailSaveCount--;
        }
    }

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
        boost::wformat(L"notebook - kicked off save of %i thumbnails.  skipped %i (no need).") %
        thumbnailSaveCount %
        (_slots.size() - thumbnailSaveCount)
        );

    return (thumbnailSaveCount > 0);
}

bool notebook::areThumbnailsSaved()
{
    for (auto &iter : _slots)
    {
        if (!iter.second.pageView->isThumbnailSaved())
        {
            return false;
        }
    }

    return true;
}

void notebook::onBeforeSettingChange(const b::wchar* fullName)
{
}

void notebook::onAfterSettingChange(const b::wchar* fullName)
{
    if (_state != state::idle) return;

    if (std::wstring(fullName).compare(constants::general::name::zoomLevel) == 0)
    {
        this->setLayoutIdWithStack(constants::general::zoomLevel());
    }
    else if (std::wstring(fullName).compare(constants::general::name::stackState) == 0)
    {
        _finalStackState = constants::general::stackState();
        this->setStackExpansion(_finalStackState);
    }
    else if (std::wstring(fullName).compare(constants::general::name::backgroundPackedColor) == 0)
    {
        // if the background color is changed, also change the shadow color
        b::float32 r = g::unpackColor_R(constants::general::backgroundPackedColor()) * constants::general::shadowDarkness();
        b::float32 g = g::unpackColor_G(constants::general::backgroundPackedColor()) * constants::general::shadowDarkness();
        b::float32 b = g::unpackColor_B(constants::general::backgroundPackedColor()) * constants::general::shadowDarkness();

        g::packed_color c = g::packColor(255, (b::uint8)r, (b::uint8)g, (b::uint8)b);
        constants::settings()->setInt((int)constants::general::id::shadowPackedColor, static_cast<int>(c));
    }
}

void notebook::onShareImageAsync(copy_info& info, std::function<void(const std::shared_ptr<view::ishared_image>&)> f)
{
	if ((_state == state::selectingWithLasso || _state == state::selectingWithLassoIdle) &&
		!this->getSelection(info.dimensions, info.origin))
	{
		this->saveSharedImageAsync(constants::general::sharedPageFileName(), f);
	}
	else
	{
		_slots[_pageNumber].pageView->onShareImageAsync(info, f);
	}
}

void notebook::onCopyImageAsync(copy_info& info, std::function<void(const std::shared_ptr<view::ishared_image>&)> f)
{
    // if there's a selection do the copy here, otherwise let the current page handle it (it's a full page copy)
    if (info.selection)
    {
        this->saveSharedImageAsync(constants::general::sharedPageFileName(), f);
    }
    else
    {
        _slots[_pageNumber].pageView->onCopyImageAsync(info, f);
    }
}

void notebook::onPasteImageAsync(const std::shared_ptr<view::ishared_image>& image, std::function<void()> f)
{
    if (_layoutId != layout_id::grid)
    {
        _slots[_pageNumber].pageView->onPasteImageAsync(image, f);
    }
}

void notebook::onCommand(const view::command_id id, void* context)
{
    bool* handled = static_cast<bool*>(context);

    if (id == view::command_id::add_page)
    {
        this->addNewPage();
        *handled = true;
    }
    else if (id == view::command_id::ok_page)
    {
        this->deselectAll();
        *handled = true;
    }
    else if (id == view::command_id::tool_eraser)
    {
        if (_layoutId != layout_id::grid)
        {
            DEBUG_CONSOLE_TRACE("Selecting the eraser tool");
            _slots[_pageNumber].pageView->onSelectInkTool(ink_surface_tool::eraser_tool);
        }
        *handled = true;
    }
    else if (id == view::command_id::tool_pen)
    {
        if (_layoutId != layout_id::grid)
        {
            DEBUG_CONSOLE_TRACE("Selecting the pen tool");
            _slots[_pageNumber].pageView->onSelectInkTool(ink_surface_tool::drawing_tool);
        }
        *handled = true;
    }
    else if (id == view::command_id::tool_selection)
    {
        DEBUG_CONSOLE_TRACE("Selecting the lasso tool");
        this->startLasso();
        *handled = true;
    }
    else if (id == view::command_id::ok_selection)
    {
        DEBUG_CONSOLE_TRACE("Finished selection");
        this->endLasso();
        *handled = true;
    }
    else if (
        id == view::command_id::zoom_fit_to_width ||
        id == view::command_id::zoom_full_page ||
        id == view::command_id::zoom_grid
        )
    {
        DEBUG_CONSOLE_TRACE("Selecting the pen tool");
        this->setLayoutIdWithStack(common::commandIdToLayoutId(id));
        *handled = true;
    }
    else if (id == view::command_id::expand_stack)
    {
        if (_layoutId != layout_id::grid)
        {
            _finalStackState = (constants::general::stackState() == view::stack_state::expanded) ? 
                view::stack_state::stacked :
                view::stack_state::expanded;

            this->setStackExpansion(_finalStackState);
        }
        *handled = true;
    }
    else if (id == view::command_id::touch_mode_off)
    {
        constants::settings()->setBool((int)constants::ink::id::touchInputMode, false);
        *handled = true;
    }
    else if (id == view::command_id::touch_mode_on)
    {
        constants::settings()->setBool((int)constants::ink::id::touchInputMode, true);
        *handled = true;
    }
    else if (id == view::command_id::delete_page)
    {
        if (_selections.size() == 0)
        {
            constants::uiService()->showDialog(L"Please select some pages before deleting them.");
            return;
        }

        if (_selections.size() == _notebookModel->pageCount())
        {
            constants::uiService()->showDialog(L"Sorry but we can't delete all the pages in the journal, there has to be at least one remaining.");
            return;
        }

        _state = state::deletingPages;
        constants::uiService()->enableAppBarButton(a::app_bar_button_id::delete_page, false);
        constants::uiService()->enableAppBarButton(a::app_bar_button_id::ok_page, false);

        _randAngles.clear();
        for (auto &iter : _selections)
        {
            size_t pageNumber = iter.first;
            _randAngles[pageNumber].x = (float)(rand() % 360);
            _randAngles[pageNumber].y = (float)(rand() % 360);
            _randAngles[pageNumber].z = (float)(rand() % 360);
        }

        b::float32 t = constants::general::currentTime();
        _transitionProgress = 0.f;
        _transitionAnimationEndTime = t + constants::animations::pageDeleteDuration();
        _transitionAnimation = b::tv_interpolation(
            t,
            _transitionAnimationEndTime,
            _transitionProgress,
            1 - _transitionProgress,
            constants::animations::pageMoveAcceleration().x,
            constants::animations::pageMoveAcceleration().y
            );
    }
    else if (id == view::command_id::delete_photo)
    {
        DEBUG_CONSOLE_TRACE("Selecting the eraser tool");
        _slots[_pageNumber].pageView->onRemoveSelectedPhoto();
        *handled = true;
    }
    else if (id == view::command_id::paper_0 || 
        id == view::command_id::paper_1 || 
        id == view::command_id::paper_2 || 
        id == view::command_id::paper_3 || 
        id == view::command_id::paper_4)
    {
        // change the default
        paper_type paperType = common::commandIdToPaperType(id);
        constants::settings()->setEnum((int)constants::general::id::paperType, static_cast<int>(paperType));

        if (_layoutId != layout_id::grid)
        {
            _slots[_pageNumber].pageView->model()->setPaperType(static_cast<b::uint32>(paperType));
        }
    }
    else // pass it along to the selected page
    {
        _slots[_pageNumber].pageView->onCommand(id, context);
    }
}

void notebook::startLasso()
{
    if (_state != state::idle)
    {
        return;
    }

    _state = state::selectingWithLassoIdle;
    constants::uiService()->hideAppBar();
    constants::uiService()->showAppBar(a::app_bar_id::lasso);

    // start selecting a 100x100 section in the middle of the screen
    //_lassoStart = m::point2d(
    //    constants::shellService()->windowSize().x / 2 - 50, 
    //    constants::shellService()->windowSize().y / 2 - 50
    //    );
    //_lassoEnd = m::point2d(
    //    constants::shellService()->windowSize().x / 2 + 50, 
    //    constants::shellService()->windowSize().y / 2 + 50
    //    );
}

void notebook::endLasso()
{
    if (_state != state::selectingWithLasso && _state != state::selectingWithLassoIdle)
    {
        return;
    }

    constants::uiService()->hideAppBar();

    this->goIdle();
}

bool notebook::handleLasso(const a::pointer_args& args)
{
    bool returnValue = false;

    if (_state == state::idle || _state == state::selectingWithLasso || _state == state::selectingWithLassoIdle)
    {
        if (args.type == a::pointer_event_type::down)
        {
            _state = state::selectingWithLasso;
            constants::uiService()->hideAppBar();
            _lassoStart = _lassoEnd = args.point;
            returnValue = true;
        }
        else if (args.type == a::pointer_event_type::up)
        {
            _state = state::selectingWithLassoIdle;
            constants::uiService()->showAppBar(a::app_bar_id::lasso);
            returnValue = true;
        }
        else if (args.type == a::pointer_event_type::move)
        {
            if (_state == state::selectingWithLasso)
            {
                _lassoEnd = args.point;
            }
            returnValue = true;
        }
    }

    return returnValue;
}

void notebook::updateScrollBars()
{
    m::point2d position;
    m::point2d dimensions;

    _layoutData[_layoutId]->layout->calculateScroll(
        _sceneState.getCamera()->offsetXY(),
        _sceneState.getCamera()->focus(),
        dimensions,
        position
        );

    _scrollBars->set(position, dimensions);
}

void notebook::onAppBarVisibilityChange(const bool visible)
{
    ACQUIRE_VIEW_LOCK;

    if (_layoutId != layout_id::grid) return;

    if (visible)
    {
        // when in grid mode, til the camera a bit in 3d to let the app bar show
        _notebookCamera->setAngles(m::point3d(35.f, 0, 0), constants::animations::quickDuration());
        _notebookCamera->setOffsetXY(m::point2d(0, -2.5f), constants::animations::quickDuration(), constants::animations::defaultAcceleration(), true); // delta
    }
    else
    {
        _notebookCamera->setOffsetXY(m::point2d(0, 0.f), constants::animations::quickDuration(), constants::animations::defaultAcceleration(), true); // delta
        _notebookCamera->moveToPage(this->layout(), _pageNumber, false, constants::animations::quickDuration(), constants::animations::defaultAcceleration());
    }
}

b::uint32 notebook::addNewPage()
{
    DEBUG_CONSOLE_TRACE("Adding new page");
    return _notebookModel->createPageModel();
}

void notebook::autoCurlThisPageAway(bool forwards)
{
    _state = state::tapCurlTransition;
    _tapCurlTransitionForwards = forwards;

	// TRACK: should use different vars for this (not _openingendtime)
    b::float32 t = constants::general::currentTime();
    _openingEndTime = t + constants::animations::tapCurlDuration();

    if (!forwards)
    {
        this->enableSlot(_pageNumber-1);
        _pageNumber--;
    }

    const std::shared_ptr<paper_sheet_node> &pageNode = 
        std::static_pointer_cast<paper_sheet_node>(_slots[_pageNumber].pageView->sceneNode());

    b::float32 min, max;
    if (forwards)
    {
        min = (pageNode->width() - pageNode->offset()) / 2.0f;
        max = -min - 2.0f;
    }
    else
    {
        min = -(pageNode->width() - pageNode->offset()) / 2.0f;
        max = -min ; // gotta decurl the whooole way
        _slots[_pageNumber].pageView->setAlpha(0.0); // make sure it starts off transparent
    }

    _startCurlAnimation = b::linear_interpolation(t, _openingEndTime, min, max);
    _pageCurl->attachPage(pageNode, false, page_curl::paper_type::looseLeaf);
    pageNode->setCurling(true);
}

void notebook::updateCameraService()
{
    // how many pixels in 1 world unit (assuming camera pointing at a page in the XY plane!!)
    _pixelToWorldRatio = constants::shellService()->windowSize().x / _sceneState.getCamera()->focus().x;

    // also make sure to re-calculate the point minimum distance based on this new ratio
    constants::settings()->setFloat((int)constants::ink::id::pointMinimumDistance, this->pixelToWorld(1.f));
}

void notebook::renderLasso()
{
    m::point2d dimensions;
    m::point2d origin;

    bool isEmpty = this->getSelection(dimensions, origin);

    constants::uiService()->enableAppBarButton(a::app_bar_button_id::copy_selection, !isEmpty);

    static const b::float32 lassoWidth = 5.f;

    if (!isEmpty)
    {
        m::point2d d;
        m::point2d o;
        bool horizontal;

        constants::graphics()->setZBuffer(g::enabled_state::disabled);
        constants::graphics()->setCulling(g::cull_state::none);

        // left border
        o.x = origin.x - dimensions.x / 2.f + lassoWidth / 2.f;
        o.y = origin.y + lassoWidth / 2.f;
        d.x = lassoWidth;
        d.y = dimensions.y - lassoWidth;
        horizontal = false;
        quad_renderer::singleton()->render(
            0.5f,
            0.f, // angle
            constants::shellService()->windowSize(),
            d,
            o,
            nullptr,
            view::post_processing_effect::lasso,
            true, // write alpha
            static_cast<const void*>(&horizontal)
            );

        // top border
        o.x = origin.x + lassoWidth / 2.f;
        o.y = origin.y + dimensions.y / 2.f - lassoWidth / 2.f;
        d.x = dimensions.x - lassoWidth;
        d.y = lassoWidth;
        horizontal = true;
        quad_renderer::singleton()->render(
            0.5f,
            0.f, // angle
            constants::shellService()->windowSize(),
            d,
            o,
            nullptr,
            view::post_processing_effect::lasso,
            true, // write alpha
            static_cast<const void*>(&horizontal)
            );

        // right border
        o.x = origin.x + dimensions.x / 2.f - lassoWidth / 2.f;
        o.y = origin.y - lassoWidth / 2.f;
        d.x = lassoWidth;
        d.y = dimensions.y - lassoWidth;
        horizontal = false;
        quad_renderer::singleton()->render(
            0.5f,
            0.f, // angle
            constants::shellService()->windowSize(),
            d,
            o,
            nullptr,
            view::post_processing_effect::lasso,
            true, // write alpha
            static_cast<const void*>(&horizontal)
            );

        // bottom border
        o.x = origin.x - lassoWidth / 2.f;
        o.y = origin.y - dimensions.y / 2.f + lassoWidth / 2.f;
        d.x = dimensions.x - lassoWidth;
        d.y = lassoWidth;
        horizontal = true;
        quad_renderer::singleton()->render(
            0.5f,
            0.f, // angle
            constants::shellService()->windowSize(),
            d,
            o,
            nullptr,
            view::post_processing_effect::lasso,
            true, // write alpha
            static_cast<const void*>(&horizontal)
            );
    }
}

bool notebook::getSelection(m::point2d& dimensions, m::point2d& origin)
{
    b::float32 left, top;

    if (_lassoStart.x < _lassoEnd.x)
    {
        left = _lassoStart.x;
        dimensions.x = _lassoEnd.x - _lassoStart.x;
    }
    else
    {
        left = _lassoEnd.x;
        dimensions.x = _lassoStart.x - _lassoEnd.x;
    }

    // inverse the y so 0 is at bottom
    b::float32 lassoStartY = constants::shellService()->windowSize().y - _lassoStart.y;
    b::float32 lassoEndY = constants::shellService()->windowSize().y - _lassoEnd.y;

    if (lassoStartY > lassoEndY)
    {
        top = lassoStartY;
        dimensions.y = lassoStartY - lassoEndY;
    }
    else
    {
        top = lassoEndY;
        dimensions.y = lassoEndY - lassoStartY;
    }

    origin.x = left - constants::shellService()->windowSize().x / 2.f + (dimensions.x / 2.f);
    origin.y = top - constants::shellService()->windowSize().y / 2.f- (dimensions.y / 2.f);

    return (dimensions.x == 0 || dimensions.y == 0); // empty ?
}

// NOTE: always generate image from selection
void notebook::saveSharedImageAsync(const std::wstring& fileName, std::function<void(const std::shared_ptr<ishared_image>&)> f)
{
    // TRACK: we are generating a file during copy but this is not necessary, this is plainly because
    // I am being lazy and I just want to reuse the "share" code as is
    winrt::saveImageAsync(
        fileName.c_str(),
        this->generateImageFromSelection(),
        default_folder::app,
        [f, fileName](const std::shared_ptr<imemory_blob>& blob)
        {
            // don't bother generating a thumbnail since we are sharing as a file we'll have winrt
            // generate a thumb from the file for us
            std::shared_ptr<shared_image> sharedImage = std::make_shared<shared_image>(
                nullptr, // thumbnail
                blob,
                fileName.c_str()
                );

            f(std::dynamic_pointer_cast<ishared_image>(sharedImage));
        });
}

std::shared_ptr<imemory_blob> notebook::generateImageFromSelection()
{
    // TRACK:
    // I am rendering a full frame into a temporary buffer and then copying a sub region from there
    // I am sure there're more efficient ways of doing this by rendering directly into the final buffer...

    std::shared_ptr<g::itexture> sourceTexture;

    m::point2d dimensions;
    m::point2d origin;

    bool isEmpty = this->getSelection(dimensions, origin);
    if (isEmpty) return nullptr;

    // create a single mip level texture as a destination for the image
    std::shared_ptr<g::itexture> texture = constants::graphics()->createTexture(
        static_cast<b::uint32>(dimensions.x),
        static_cast<b::uint32>(dimensions.y),
        1, // mip map level
        static_cast<b::uint32>(g::texture_usage::renderTarget),
        g::pixel_format::d32BitColorNoAlpha,
        g::memory_pool::default,
        1, // single sampled
        0
        );

    // create a texture to contain the current full frame
    std::shared_ptr<g::itexture> frameTexture = constants::graphics()->createTexture(
        static_cast<b::uint32>(constants::shellService()->windowSize().x),
        static_cast<b::uint32>(constants::shellService()->windowSize().y),
        1, // mip map level
        static_cast<b::uint32>(g::texture_usage::renderTarget),
        g::pixel_format::d32BitColorNoAlpha,
        g::memory_pool::default,
        1, // single sampled
        0
        );

    // create the depth stencil
    std::shared_ptr<g::isurface> depthStencil =
        constants::graphics()->createDepthStencilSurface(
            static_cast<b::uint32>(constants::shellService()->windowSize().x),
            static_cast<b::uint32>(constants::shellService()->windowSize().y),
            constants::graphics()->getDisplayParams().depthStencilFormat,
            false // don't use fsaa
            );

    // snap current target
    g::snap_render_target snapRenderTarget(constants::graphics());

    // create new render target for the frame
    g::viewport vp = common::getViewport(constants::shellService()->windowSize());
    std::shared_ptr<g::render_target> frameRenderTarget = std::make_shared<g::render_target>(vp, frameTexture, depthStencil);

    // create render target for the final texture (clipped from the full frame)
    vp = common::getViewport(dimensions);
    std::shared_ptr<g::render_target> renderTarget = std::make_shared<g::render_target>(vp, texture, depthStencil);

    // render the frame
    frameRenderTarget->apply(constants::graphics());
    this->renderFrame(false); // without chrome

    // need to map now to texture coordinates (normalized)
    m::point2d sourcePosition;
    m::point2d destPosition(0.f, 0.f);

    sourcePosition.x = origin.x + constants::shellService()->windowSize().x / 2.f - (dimensions.x / 2.f);
    sourcePosition.y = origin.y + constants::shellService()->windowSize().y / 2.f + (dimensions.y / 2.f);
    sourcePosition.y = constants::shellService()->windowSize().y - sourcePosition.y;

    dx11_1::copyRectangle(
        frameTexture,
        texture,
        sourcePosition,
        destPosition
        );

    std::shared_ptr<dx11_1::thumbnail_data> td = std::make_shared<dx11_1::thumbnail_data>(texture, g::file_format::JPG);
    return std::dynamic_pointer_cast<imemory_blob>(td);
}

void notebook::onPhotoFileSelected(const std::shared_ptr<b::storage::istorage_file>& file)
{
    std::shared_ptr<ipage> pageView = _slots[_pageNumber].pageView;
    pageView->addPhotoWithScale(file);
}

std::shared_ptr<ipage> notebook::pageFromId(const b::uint32& pageId)
{
    std::shared_ptr<ipage> pageView;
    if (!_pageCache.tryGet(pageId, &pageView))
    {
        return nullptr;
    }

    size_t  pageNumber = _notebookModel->getPageNumberFromPageId(pageId);
    enableSlot(pageNumber, false);

    return pageView;
}

}} // namespace journal::view
